neovim

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

buffer_spec.lua (63913B)


      1 local t = require('test.testutil')
      2 local n = require('test.functional.testnvim')()
      3 local Screen = require('test.functional.ui.screen')
      4 local tt = require('test.functional.testterm')
      5 
      6 local assert_alive = n.assert_alive
      7 local feed, clear = n.feed, n.clear
      8 local poke_eventloop = n.poke_eventloop
      9 local nvim_prog = n.nvim_prog
     10 local eval, source = n.eval, n.source
     11 local pcall_err = t.pcall_err
     12 local eq, neq = t.eq, t.neq
     13 local api = n.api
     14 local retry = t.retry
     15 local testprg = n.testprg
     16 local write_file = t.write_file
     17 local command = n.command
     18 local matches = t.matches
     19 local exec_lua = n.exec_lua
     20 local sleep = vim.uv.sleep
     21 local fn = n.fn
     22 local is_os = t.is_os
     23 local skip = t.skip
     24 
     25 describe(':terminal buffer', function()
     26  local screen
     27 
     28  before_each(function()
     29    clear()
     30    command('set modifiable swapfile undolevels=20')
     31    screen = tt.setup_screen()
     32  end)
     33 
     34  it('terminal-mode forces various options', function()
     35    local expr =
     36      '[&l:cursorlineopt, &l:cursorline, &l:cursorcolumn, &l:scrolloff, &l:sidescrolloff]'
     37 
     38    feed([[<C-\><C-N>]])
     39    command('setlocal cursorline cursorlineopt=both cursorcolumn scrolloff=4 sidescrolloff=7')
     40    eq({ 'both', 1, 1, 4, 7 }, eval(expr))
     41    eq('nt', eval('mode(1)'))
     42 
     43    -- Enter Terminal mode ("insert" mode in :terminal).
     44    feed('i')
     45    eq('t', eval('mode(1)'))
     46    eq({ 'number', 1, 0, 0, 0 }, eval(expr))
     47 
     48    -- Return to Normal mode.
     49    feed([[<C-\><C-N>]])
     50    eq('nt', eval('mode(1)'))
     51    eq({ 'both', 1, 1, 4, 7 }, eval(expr))
     52 
     53    -- Enter Terminal mode again.
     54    feed('i')
     55    eq('t', eval('mode(1)'))
     56    eq({ 'number', 1, 0, 0, 0 }, eval(expr))
     57 
     58    -- Delete the terminal buffer and return to the previous buffer.
     59    command('bwipe!')
     60    feed('<Ignore>') -- Add input to separate two RPC requests
     61    eq('n', eval('mode(1)'))
     62    -- Window options in the old buffer should be unchanged. #37484
     63    eq({ 'both', 0, 0, -1, -1 }, eval(expr))
     64  end)
     65 
     66  it('terminal-mode does not change cursorlineopt if cursorline is disabled', function()
     67    feed([[<C-\><C-N>]])
     68    command('setlocal nocursorline cursorlineopt=both')
     69    feed('i')
     70    eq({ 0, 'both' }, eval('[&l:cursorline, &l:cursorlineopt]'))
     71  end)
     72 
     73  it('terminal-mode disables cursorline when cursorlineopt is only set to "line"', function()
     74    feed([[<C-\><C-N>]])
     75    command('setlocal cursorline cursorlineopt=line')
     76    feed('i')
     77    eq({ 0, 'line' }, eval('[&l:cursorline, &l:cursorlineopt]'))
     78  end)
     79 
     80  describe('swap and undo', function()
     81    before_each(function()
     82      feed('<c-\\><c-n>')
     83      screen:expect([[
     84        tty ready                                         |
     85        ^                                                  |
     86                                                          |*5
     87      ]])
     88    end)
     89 
     90    it('does not create swap files', function()
     91      eq('No swap file', n.exec_capture('swapname'))
     92    end)
     93 
     94    it('does not create undo files', function()
     95      local undofile = api.nvim_eval('undofile(bufname("%"))')
     96      eq(nil, io.open(undofile))
     97    end)
     98  end)
     99 
    100  it('cannot be modified directly', function()
    101    feed('<c-\\><c-n>dd')
    102    screen:expect([[
    103      tty ready                                         |
    104      ^                                                  |
    105                                                        |*4
    106      {101:E21: Cannot make changes, 'modifiable' is off}     |
    107    ]])
    108  end)
    109 
    110  it('sends data to the terminal when the "put" operator is used', function()
    111    feed('<c-\\><c-n>gg"ayj')
    112    fn.setreg('a', 'appended ' .. fn.getreg('a'))
    113    feed('"ap"ap')
    114    screen:expect([[
    115      ^tty ready                                         |
    116      appended tty ready                                |*2
    117                                                        |*4
    118    ]])
    119    -- operator count is also taken into consideration
    120    feed('3"ap')
    121    screen:expect([[
    122      ^tty ready                                         |
    123      appended tty ready                                |*5
    124                                                        |
    125    ]])
    126  end)
    127 
    128  it('sends data to the terminal when the ":put" command is used', function()
    129    feed('<c-\\><c-n>gg"ayj')
    130    fn.setreg('a', 'appended ' .. fn.getreg('a'))
    131    feed(':put a<CR>')
    132    screen:expect([[
    133      ^tty ready                                         |
    134      appended tty ready                                |
    135                                                        |*4
    136      :put a                                            |
    137    ]])
    138    -- line argument is only used to move the cursor
    139    feed(':6put a<CR>')
    140    screen:expect([[
    141      tty ready                                         |
    142      appended tty ready                                |*2
    143                                                        |*2
    144      ^                                                  |
    145      :6put a                                           |
    146    ]])
    147  end)
    148 
    149  it('can be deleted', function()
    150    feed('<c-\\><c-n>:bd!<cr>')
    151    screen:expect([[
    152      ^                                                  |
    153      {100:~                                                 }|*5
    154      :bd!                                              |
    155    ]])
    156    feed(':bnext<CR>')
    157    screen:expect([[
    158      ^                                                  |
    159      {100:~                                                 }|*5
    160      :bnext                                            |
    161    ]])
    162  end)
    163 
    164  it('handles loss of focus gracefully', function()
    165    -- Change the statusline to avoid printing the file name, which varies.
    166    api.nvim_set_option_value('statusline', '==========', {})
    167 
    168    -- Save the buffer number of the terminal for later testing.
    169    local tbuf = eval('bufnr("%")')
    170    local exitcmd = is_os('win') and "['cmd', '/c', 'exit']" or "['sh', '-c', 'exit']"
    171    source([[
    172    function! SplitWindow(id, data, event)
    173      new
    174      call feedkeys("iabc\<Esc>")
    175    endfunction
    176 
    177    startinsert
    178    call jobstart(]] .. exitcmd .. [[, {'on_exit': function("SplitWindow")})
    179    call feedkeys("\<C-\>", 't')  " vim will expect <C-n>, but be exited out of
    180                                  " the terminal before it can be entered.
    181    ]])
    182 
    183    -- We should be in a new buffer now.
    184    screen:expect([[
    185      ab^c                                               |
    186      {100:~                                                 }|
    187      {3:==========                                        }|
    188      rows: 2, cols: 50                                 |
    189                                                        |
    190      {119:==========                                        }|
    191                                                        |
    192    ]])
    193 
    194    neq(tbuf, eval('bufnr("%")'))
    195    command('quit!') -- Should exit the new window, not the terminal.
    196    eq(tbuf, eval('bufnr("%")'))
    197  end)
    198 
    199  describe('handles confirmations', function()
    200    it('with :confirm', function()
    201      feed('<c-\\><c-n>')
    202      feed(':confirm bdelete<CR>')
    203      screen:expect { any = 'Close "term://' }
    204    end)
    205 
    206    it('with &confirm', function()
    207      feed('<c-\\><c-n>')
    208      feed(':bdelete<CR>')
    209      screen:expect { any = 'E89' }
    210      feed('<cr>')
    211      eq('terminal', eval('&buftype'))
    212      feed(':set confirm | bdelete<CR>')
    213      screen:expect { any = 'Close "term://' }
    214      feed('y')
    215      neq('terminal', eval('&buftype'))
    216    end)
    217  end)
    218 
    219  it('it works with set rightleft #11438', function()
    220    local columns = eval('&columns')
    221    feed(string.rep('a', columns))
    222    command('set rightleft')
    223    screen:expect([[
    224                                               ydaer ytt|
    225      ^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|
    226                                                        |*4
    227      {5:-- TERMINAL --}                                    |
    228    ]])
    229    command('bdelete!')
    230  end)
    231 
    232  it('requires bang (!) to close a running job #15402', function()
    233    eq('Vim(wqall):E948: Job still running (add ! to end the job)', pcall_err(command, 'wqall'))
    234    for _, cmd in ipairs({ 'bdelete', '%bdelete', 'bwipeout', 'bunload' }) do
    235      matches(
    236        '^Vim%('
    237          .. cmd:gsub('%%', '')
    238          .. '%):E89: term://.*tty%-test.* will be killed %(add %! to override%)$',
    239        pcall_err(command, cmd)
    240      )
    241    end
    242    command('call jobstop(&channel)')
    243    assert(0 >= eval('jobwait([&channel], 1000)[0]'))
    244    command('bdelete')
    245  end)
    246 
    247  it(':wqall! closes a running job', function()
    248    n.expect_exit(command, 'wqall!')
    249  end)
    250 
    251  it('stops running jobs with :quit', function()
    252    -- Open in a new window to avoid terminating the nvim instance
    253    command('split')
    254    command('terminal')
    255    command('set nohidden')
    256    command('quit')
    257  end)
    258 
    259  it('does not segfault when pasting empty register #13955', function()
    260    feed('<c-\\><c-n>')
    261    command('put a') -- register a is empty
    262    n.assert_alive()
    263  end)
    264 
    265  it([[can use temporary normal mode <c-\><c-o>]], function()
    266    eq('t', fn.mode(1))
    267    feed [[<c-\><c-o>]]
    268    screen:expect([[
    269      tty ready                                         |
    270      ^                                                  |
    271                                                        |*4
    272      {5:-- (terminal) --}                                  |
    273    ]])
    274    eq('ntT', fn.mode(1))
    275 
    276    feed [[:let g:x = 17]]
    277    screen:expect([[
    278      tty ready                                         |
    279                                                        |*5
    280      :let g:x = 17^                                     |
    281    ]])
    282 
    283    feed [[<cr>]]
    284    screen:expect([[
    285      tty ready                                         |
    286      ^                                                  |
    287                                                        |*4
    288      {5:-- TERMINAL --}                                    |
    289    ]])
    290    eq('t', fn.mode(1))
    291  end)
    292 
    293  it('is refreshed with partial mappings in Terminal mode #9167', function()
    294    command([[set timeoutlen=20000 | tnoremap jk <C-\><C-N>]])
    295    feed('j') -- Won't reach the terminal until the next character is typed
    296    screen:expect_unchanged()
    297    feed('j') -- Refresh scheduled for the first 'j' but not processed
    298    screen:expect_unchanged()
    299    for i = 1, 10 do
    300      eq({ mode = 't', blocking = true }, api.nvim_get_mode())
    301      vim.uv.sleep(15) -- Wait for the previously scheduled refresh timer to arrive
    302      feed('j') -- Refresh scheduled for the last 'j' and processed for the one before
    303      screen:expect(([[
    304        tty ready                                         |
    305        %s^%s|
    306                                                          |*4
    307        {5:-- TERMINAL --}                                    |
    308      ]]):format(('j'):rep(i), (' '):rep(50 - i)))
    309    end
    310    feed('l') -- No partial mapping, so all pending refreshes should be processed
    311    screen:expect([[
    312      tty ready                                         |
    313      jjjjjjjjjjjjl^                                     |
    314                                                        |*4
    315      {5:-- TERMINAL --}                                    |
    316    ]])
    317  end)
    318 
    319  it('is refreshed with partial mappings in Normal mode', function()
    320    command('set timeoutlen=20000 | nnoremap jk :')
    321    command('nnoremap j <Cmd>call chansend(&channel, "j")<CR>')
    322    feed([[<C-\><C-N>]])
    323    screen:expect([[
    324      tty ready                                         |
    325      ^                                                  |
    326                                                        |*5
    327    ]])
    328    feed('j') -- Won't reach the terminal until the next character is typed
    329    screen:expect_unchanged()
    330    feed('j') -- Refresh scheduled for the first 'j' but not processed
    331    screen:expect_unchanged()
    332    for i = 1, 10 do
    333      eq({ mode = 'nt', blocking = true }, api.nvim_get_mode())
    334      vim.uv.sleep(15) -- Wait for the previously scheduled refresh timer to arrive
    335      feed('j') -- Refresh scheduled for the last 'j' and processed for the one before
    336      screen:expect(([[
    337        tty ready                                         |
    338        ^%s%s|
    339                                                          |*5
    340      ]]):format(('j'):rep(i), (' '):rep(50 - i)))
    341    end
    342    feed('l') -- No partial mapping, so all pending refreshes should be processed
    343    screen:expect([[
    344      tty ready                                         |
    345      j^jjjjjjjjjjj                                      |
    346                                                        |*5
    347    ]])
    348  end)
    349 
    350  it('writing to an existing file with :w fails #13549', function()
    351    eq(
    352      'Vim(write):E13: File exists (add ! to override)',
    353      pcall_err(command, 'write test/functional/fixtures/tty-test.c')
    354    )
    355  end)
    356 
    357  it('external interrupt (got_int) does not hang #20726', function()
    358    eq({ mode = 't', blocking = false }, api.nvim_get_mode())
    359    command('call timer_start(0, {-> interrupt()})')
    360    feed('<Ignore>') -- Add input to separate two RPC requests
    361    eq({ mode = 't', blocking = false }, api.nvim_get_mode())
    362    feed([[<C-\><C-N>]])
    363    eq({ mode = 'nt', blocking = false }, api.nvim_get_mode())
    364    command('bd!')
    365  end)
    366 
    367  it('correct size when switching buffers', function()
    368    local term_buf = api.nvim_get_current_buf()
    369    command('file foo | enew | vsplit')
    370    api.nvim_set_current_buf(term_buf)
    371    screen:expect([[
    372      tty ready                                        |
    373      ^rows: 5, cols: 25        │{100:~                       }|
    374                               │{100:~                       }|*3
    375      {120:foo [-]                   }{2:[No Name]               }|
    376                                                        |
    377    ]])
    378 
    379    feed('<C-^><C-W><C-O><C-^>')
    380    screen:expect([[
    381      tty ready                                         |
    382      ^rows: 5, cols: 25                                 |
    383      rows: 6, cols: 50                                 |
    384                                                        |*4
    385    ]])
    386  end)
    387 
    388  it('reports focus notifications when requested', function()
    389    feed([[<C-\><C-N>]])
    390    exec_lua(function()
    391      local function new_test_term()
    392        local chan = vim.api.nvim_open_term(0, {
    393          on_input = function(_, term, buf, data)
    394            if data == '\27[I' then
    395              vim.b[buf].term_focused = true
    396              vim.api.nvim_chan_send(term, 'focused\n')
    397            elseif data == '\27[O' then
    398              vim.b[buf].term_focused = false
    399              vim.api.nvim_chan_send(term, 'unfocused\n')
    400            end
    401          end,
    402        })
    403        vim.b.term_focused = false
    404        vim.api.nvim_chan_send(chan, '\27[?1004h') -- Enable focus reporting
    405      end
    406 
    407      vim.cmd 'edit bar'
    408      new_test_term()
    409      vim.cmd 'vnew foo'
    410      new_test_term()
    411      vim.cmd 'vsplit'
    412    end)
    413    screen:expect([[
    414      ^                                                |
    415                                                      |*4
    416      {120:foo [-]              }{119:foo [-]        bar [-]       }|
    417                                                        |
    418    ]])
    419 
    420    -- TermEnter/Leave happens *after* entering/leaving terminal mode, so focus should've changed
    421    -- already by the time these events run.
    422    exec_lua(function()
    423      _G.last_event = nil
    424      vim.api.nvim_create_autocmd({ 'TermEnter', 'TermLeave' }, {
    425        callback = function(args)
    426          _G.last_event = args.event
    427            .. ' '
    428            .. vim.fs.basename(args.file)
    429            .. ' '
    430            .. tostring(vim.b[args.buf].term_focused)
    431        end,
    432      })
    433    end)
    434 
    435    feed('i')
    436    screen:expect([[
    437      focused             focused                     |
    438      ^                                                |
    439                                                      |*3
    440      {120:foo [-]              }{119:foo [-]        bar [-]       }|
    441      {5:-- TERMINAL --}                                    |
    442    ]])
    443    eq('TermEnter foo true', exec_lua('return _G.last_event'))
    444 
    445    -- Next window has the same terminal; no new notifications.
    446    command('wincmd w')
    447    screen:expect([[
    448      focused             focused                     |
    449                          ^                            |
    450                                                      |*3
    451      {119:foo [-]              }{120:foo [-]              }{119:bar [-] }|
    452      {5:-- TERMINAL --}                                    |
    453    ]])
    454    -- Next window has a different terminal; expect new unfocus and focus notifications.
    455    command('wincmd w')
    456    screen:expect([[
    457      focused             focused focused             |
    458      unfocused           unfocuse^                    |
    459                                                      |*3
    460      {119:foo [-]              foo [-]  }{120:bar [-]             }|
    461      {5:-- TERMINAL --}                                    |
    462    ]])
    463    -- Leaving terminal mode; expect a new unfocus notification.
    464    feed([[<C-\><C-N>]])
    465    screen:expect([[
    466      focused             focused focused             |
    467      unfocused           unfocuseunfocused           |
    468                                  ^                    |
    469                                                      |*2
    470      {119:foo [-]              foo [-]  }{120:bar [-]             }|
    471                                                        |
    472    ]])
    473    eq('TermLeave bar false', exec_lua('return _G.last_event'))
    474  end)
    475 
    476  it('no crash with race between buffer close and OSC 2', function()
    477    skip(is_os('win'), 'tty-test cannot forward OSC 2 on Windows?')
    478    exec_lua(function()
    479      vim.api.nvim_chan_send(vim.bo.channel, '\027]2;SOME_TITLE\007')
    480    end)
    481    retry(nil, 4000, function()
    482      eq('SOME_TITLE', api.nvim_buf_get_var(0, 'term_title'))
    483    end)
    484    screen:expect_unchanged()
    485    --- @type string
    486    local title_before_del = exec_lua(function()
    487      vim.wait(10) -- Ensure there are no pending events so that a write isn't queued.
    488      vim.api.nvim_chan_send(vim.bo.channel, '\027]2;OTHER_TITLE\007')
    489      vim.uv.sleep(50) -- Block the event loop and wait for tty-test to forward OSC 2.
    490      local term_title = vim.b.term_title
    491      vim.api.nvim_buf_delete(0, { force = true })
    492      vim.wait(10, nil, nil, true) -- Process fast events only.
    493      return term_title
    494    end)
    495    -- Title isn't changed until the second vim.wait().
    496    eq('SOME_TITLE', title_before_del)
    497    screen:expect([[
    498      ^                                                  |
    499      {100:~                                                 }|*5
    500                                                        |
    501    ]])
    502    assert_alive()
    503  end)
    504 
    505  describe('handles suspended PTY process', function()
    506    if skip(is_os('win'), 'N/A for Windows') then
    507      return
    508    end
    509 
    510    --- @param external_resume boolean
    511    local function test_term_process_suspend_resume(external_resume)
    512      command('set mousemodel=extend')
    513      local pid = eval('jobpid(&channel)')
    514      vim.uv.kill(pid, 'sigstop')
    515      -- In Terminal mode the cursor is at the "[Process suspended]" text to hint that
    516      -- pressing a key will change the suspended state.
    517      local s0 = [[
    518        tty ready                                         |
    519                                                          |*4
    520        ^[Process suspended]                               |
    521        {5:-- TERMINAL --}                                    |
    522      ]]
    523      screen:expect(s0)
    524      feed([[<C-\><C-N>]])
    525      -- Returning to Normal mode puts the cursor at its previous position as that's
    526      -- closer to the terminal output, making it easier for the user to copy.
    527      screen:expect([[
    528        tty ready                                         |
    529        ^                                                  |
    530                                                          |*3
    531        [Process suspended]                               |
    532                                                          |
    533      ]])
    534      feed('i')
    535      screen:expect(s0)
    536      command('set laststatus=0 | botright vsplit')
    537      screen:expect([[
    538        tty ready               tty ready                |
    539                                                         |*4
    540        [Process suspended]     ^[Process suspended]      |
    541        {5:-- TERMINAL --}                                    |
    542      ]])
    543      -- Resize is detected by the process on resume.
    544      if external_resume then
    545        vim.uv.kill(pid, 'sigcont')
    546      else
    547        feed('a')
    548      end
    549      screen:expect([[
    550        tty ready               tty ready                |
    551        rows: 6, cols: 25       rows: 6, cols: 25        |
    552                                │^                         |
    553                                                         |*3
    554        {5:-- TERMINAL --}                                    |
    555      ]])
    556      tt.enable_mouse()
    557      tt.feed_data('mouse enabled\n\n\n\n')
    558      screen:expect([[
    559        rows: 6, cols: 25       rows: 6, cols: 25        |
    560        mouse enabled           mouse enabled            |
    561                                                         |*3
    562                                ^                         |
    563        {5:-- TERMINAL --}                                    |
    564      ]])
    565      api.nvim_input_mouse('right', 'press', '', 0, 0, 25)
    566      screen:expect({ any = vim.pesc('"!!^') })
    567      api.nvim_input_mouse('right', 'release', '', 0, 0, 25)
    568      screen:expect({ any = vim.pesc('#!!^') })
    569      vim.uv.kill(pid, 'sigstop')
    570      local s1 = [[
    571        rows: 6, cols: 25       │rows: 6, cols: 25        |
    572        mouse enabled           │mouse enabled            |
    573                                │                         |*3
    574        [Process suspended]     │^[Process suspended]      |
    575        {5:-- TERMINAL --}                                    |
    576      ]]
    577      screen:expect(s1)
    578      -- Mouse isn't forwarded when process is suspended.
    579      api.nvim_input_mouse('right', 'press', '', 0, 1, 27)
    580      api.nvim_input_mouse('right', 'release', '', 0, 1, 27)
    581      screen:expect([[
    582        rows: 6, cols: 25       rows: 6, cols: 25        |
    583        mo{108:use enabled}           mo^u{108:se enabled}            |
    584        {108: }                       {108: }                        |*3
    585        [Process suspended]     [Process suspended]      |
    586        {5:-- VISUAL --}                                      |
    587      ]])
    588      feed('<Esc>i')
    589      screen:expect(s1)
    590      if external_resume then
    591        vim.uv.kill(pid, 'sigcont')
    592      else
    593        feed('a')
    594      end
    595      screen:expect([[
    596        rows: 6, cols: 25       rows: 6, cols: 25        |
    597        mouse enabled           mouse enabled            |
    598                                                         |*3
    599           #!!                     #!!^                   |
    600        {5:-- TERMINAL --}                                    |
    601      ]])
    602      -- Mouse is forwarded after process is resumed.
    603      api.nvim_input_mouse('right', 'press', '', 0, 0, 28)
    604      screen:expect({ any = vim.pesc('"$!^') })
    605      api.nvim_input_mouse('right', 'release', '', 0, 0, 28)
    606      screen:expect({ any = vim.pesc('#$!^') })
    607    end
    608 
    609    it('resumed by an external signal', function()
    610      skip(is_os('mac'), 'FIXME: does not work on macOS')
    611      test_term_process_suspend_resume(true)
    612    end)
    613 
    614    it('resumed by pressing a key', function()
    615      test_term_process_suspend_resume(false)
    616    end)
    617  end)
    618 end)
    619 
    620 describe(':terminal buffer', function()
    621  before_each(clear)
    622 
    623  it('can resume suspended PTY process running in fish', function()
    624    skip(is_os('win'), 'N/A for Windows')
    625    skip(fn.executable('fish') == 0, 'missing "fish" command')
    626 
    627    local screen = Screen.new(50, 7)
    628    screen:add_extra_attr_ids({
    629      [100] = {
    630        foreground = Screen.colors.NvimDarkGrey2,
    631        background = Screen.colors.NvimLightGrey2,
    632      },
    633      [101] = {
    634        foreground = Screen.colors.NvimLightGrey4,
    635        background = Screen.colors.NvimLightGrey2,
    636      },
    637      [102] = {
    638        foreground = Screen.colors.NvimDarkGrey2,
    639        background = Screen.colors.NvimLightGrey4,
    640      },
    641    })
    642    command('set shell=fish termguicolors')
    643    command(('terminal %s -u NONE -i NONE'):format(fn.shellescape(nvim_prog)))
    644    command('startinsert')
    645    local s0 = [[
    646      {100:^                                                  }|
    647      {101:~                                                 }|*3
    648      {102:[No Name]                       0,0-1          All}|
    649      {100:                                                  }|
    650      {5:-- TERMINAL --}                                    |
    651    ]]
    652    screen:expect(s0)
    653    feed('<C-Z>')
    654    screen:expect([[
    655                                                        |*5
    656      ^[Process suspended]                               |
    657      {5:-- TERMINAL --}                                    |
    658    ]])
    659    feed('<Space>')
    660    screen:expect(s0)
    661  end)
    662 
    663  it('term_close() use-after-free #4393', function()
    664    command('terminal yes')
    665    feed('<Ignore>') -- Add input to separate two RPC requests
    666    command('bdelete!')
    667  end)
    668 
    669  describe('TermRequest', function()
    670    it('emits events #26972', function()
    671      local term = api.nvim_open_term(0, {})
    672      local termbuf = api.nvim_get_current_buf()
    673 
    674      -- Test that <abuf> is the terminal buffer, not the current buffer
    675      command('au TermRequest * let g:termbuf = +expand("<abuf>")')
    676      command('wincmd p')
    677 
    678      -- cwd will be inserted in a file URI, which cannot contain backs
    679      local cwd = t.fix_slashes(fn.getcwd())
    680      local parent = cwd:match('^(.+/)')
    681      local expected = '\027]7;file://host' .. parent
    682      api.nvim_chan_send(term, string.format('%s\027\\', expected))
    683      eq(expected, eval('v:termrequest'))
    684      eq(termbuf, eval('g:termbuf'))
    685    end)
    686 
    687    it('emits events for APC', function()
    688      local term = api.nvim_open_term(0, {})
    689 
    690      -- cwd will be inserted in a file URI, which cannot contain backs
    691      local cwd = t.fix_slashes(fn.getcwd())
    692      local parent = cwd:match('^(.+/)')
    693      local expected = '\027_Gfile://host' .. parent
    694      api.nvim_chan_send(term, string.format('%s\027\\', expected))
    695      eq(expected, eval('v:termrequest'))
    696    end)
    697 
    698    it('synchronization #27572', function()
    699      command('autocmd! nvim.terminal TermRequest')
    700      local term = exec_lua([[
    701        _G.input = {}
    702        local term = vim.api.nvim_open_term(0, {
    703          on_input = function(_, _, _, data)
    704            table.insert(_G.input, data)
    705          end,
    706          force_crlf = false,
    707        })
    708        vim.api.nvim_create_autocmd('TermRequest', {
    709          callback = function(args)
    710            if args.data.sequence == '\027]11;?' then
    711              table.insert(_G.input, '\027]11;rgb:0000/0000/0000\027\\')
    712            end
    713          end
    714        })
    715        return term
    716      ]])
    717      api.nvim_chan_send(term, '\027]11;?\007\027[5n\027]11;?\007\027[5n')
    718      eq({
    719        '\027]11;rgb:0000/0000/0000\027\\',
    720        '\027[0n',
    721        '\027]11;rgb:0000/0000/0000\027\\',
    722        '\027[0n',
    723      }, exec_lua('return _G.input'))
    724    end)
    725 
    726    it('works with vim.wait() from another autocommand #32706', function()
    727      command('autocmd! nvim.terminal TermRequest')
    728      exec_lua([[
    729        local term = vim.api.nvim_open_term(0, {})
    730        vim.api.nvim_create_autocmd('TermRequest', {
    731          buffer = 0,
    732          callback = function(ev)
    733            _G.sequence = ev.data.sequence
    734            _G.v_termrequest = vim.v.termrequest
    735          end,
    736        })
    737        vim.api.nvim_create_autocmd('TermEnter', {
    738          buffer = 0,
    739          callback = function()
    740            vim.api.nvim_chan_send(term, '\027]11;?\027\\')
    741            _G.result = vim.wait(3000, function()
    742              local expected = '\027]11;?'
    743              return _G.sequence == expected and _G.v_termrequest == expected
    744            end)
    745          end,
    746        })
    747      ]])
    748      feed('i')
    749      retry(nil, 4000, function()
    750        eq(true, exec_lua('return _G.result'))
    751      end)
    752    end)
    753 
    754    it('includes cursor position #31609', function()
    755      command('autocmd! nvim.terminal TermRequest')
    756      local screen = Screen.new(50, 10)
    757      local term = exec_lua([[
    758        _G.cursor = {}
    759        local term = vim.api.nvim_open_term(0, {})
    760        vim.api.nvim_create_autocmd('TermRequest', {
    761          callback = function(args)
    762            _G.cursor = args.data.cursor
    763          end
    764        })
    765        return term
    766      ]])
    767      -- Enter terminal mode so that the cursor follows the output
    768      feed('a')
    769 
    770      -- Put some lines into the scrollback. This tests the conversion from terminal line to buffer
    771      -- line.
    772      api.nvim_chan_send(term, string.rep('>\n', 20))
    773      screen:expect([[
    774        >                                                 |*8
    775        ^                                                  |
    776        {5:-- TERMINAL --}                                    |
    777      ]])
    778 
    779      -- Emit an OSC escape sequence
    780      api.nvim_chan_send(term, 'Hello\nworld!\027]133;D\027\\')
    781      screen:expect([[
    782        >                                                 |*7
    783        Hello                                             |
    784        world!^                                            |
    785        {5:-- TERMINAL --}                                    |
    786      ]])
    787      eq({ 22, 6 }, exec_lua('return _G.cursor'))
    788 
    789      api.nvim_chan_send(term, '\nHello\027]133;D\027\\\nworld!\n')
    790      screen:expect([[
    791        >                                                 |*4
    792        Hello                                             |
    793        world!                                            |
    794        Hello                                             |
    795        world!                                            |
    796        ^                                                  |
    797        {5:-- TERMINAL --}                                    |
    798      ]])
    799      eq({ 23, 5 }, exec_lua('return _G.cursor'))
    800 
    801      api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(6))
    802      screen:expect([[
    803        world!                                            |
    804        Hello                                             |
    805        world!                                            |
    806                                                          |*5
    807        ^                                                  |
    808        {5:-- TERMINAL --}                                    |
    809      ]])
    810      eq({ 25, 5 }, exec_lua('return _G.cursor'))
    811 
    812      api.nvim_set_option_value('scrollback', 10, {})
    813      eq(19, api.nvim_buf_line_count(0))
    814 
    815      api.nvim_chan_send(term, 'Hello\nworld!\027]133;D\027\\')
    816      screen:expect([[
    817        Hello                                             |
    818        world!                                            |
    819                                                          |*5
    820        Hello                                             |
    821        world!^                                            |
    822        {5:-- TERMINAL --}                                    |
    823      ]])
    824      eq({ 19, 6 }, exec_lua('return _G.cursor'))
    825 
    826      api.nvim_chan_send(term, '\nHello\027]133;D\027\\\nworld!\n')
    827      screen:expect([[
    828                                                          |*4
    829        Hello                                             |
    830        world!                                            |
    831        Hello                                             |
    832        world!                                            |
    833        ^                                                  |
    834        {5:-- TERMINAL --}                                    |
    835      ]])
    836      eq({ 17, 5 }, exec_lua('return _G.cursor'))
    837 
    838      api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(6))
    839      screen:expect([[
    840        world!                                            |
    841        Hello                                             |
    842        world!                                            |
    843                                                          |*5
    844        ^                                                  |
    845        {5:-- TERMINAL --}                                    |
    846      ]])
    847      eq({ 12, 5 }, exec_lua('return _G.cursor'))
    848 
    849      api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(8))
    850      screen:expect([[
    851        world!                                            |
    852                                                          |*7
    853        ^                                                  |
    854        {5:-- TERMINAL --}                                    |
    855      ]])
    856      eq({ 10, 5 }, exec_lua('return _G.cursor'))
    857 
    858      api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(20))
    859      screen:expect([[
    860                                                          |*8
    861        ^                                                  |
    862        {5:-- TERMINAL --}                                    |
    863      ]])
    864      eq({ -2, 5 }, exec_lua('return _G.cursor'))
    865    end)
    866 
    867    it('does not cause hang in vim.wait() #32753', function()
    868      local screen = Screen.new(50, 10)
    869 
    870      exec_lua(function()
    871        local term = vim.api.nvim_open_term(0, {})
    872 
    873        -- Write OSC sequence with pending scrollback. TermRequest will
    874        -- reschedule itself onto an event queue until the pending scrollback is
    875        -- processed (i.e. the terminal is refreshed).
    876        vim.api.nvim_chan_send(term, string.format('%s\027]133;;\007', string.rep('a\n', 100)))
    877 
    878        -- vim.wait() drains the event queue. The terminal won't be refreshed
    879        -- until the event queue is empty. This test ensures that TermRequest
    880        -- does not continuously reschedule itself onto the same event queue,
    881        -- causing an infinite loop.
    882        vim.wait(100)
    883      end)
    884 
    885      screen:expect([[
    886        ^a                                                 |
    887        a                                                 |*8
    888                                                          |
    889      ]])
    890    end)
    891 
    892    describe('no heap-use-after-free after', function()
    893      local term
    894 
    895      before_each(function()
    896        term = exec_lua(function()
    897          vim.api.nvim_create_autocmd('TermRequest', { callback = function() end })
    898          return vim.api.nvim_open_term(0, {})
    899        end)
    900      end)
    901 
    902      it('wiping buffer with pending TermRequest #37226', function()
    903        exec_lua(function()
    904          vim.api.nvim_chan_send(term, '\027]8;;https://example.com\027\\')
    905          vim.api.nvim_buf_delete(0, { force = true })
    906        end)
    907        assert_alive()
    908      end)
    909 
    910      it('unloading buffer with pending TermRequest #37226', function()
    911        api.nvim_create_buf(true, false) -- Create a buffer to switch to.
    912        exec_lua(function()
    913          vim.api.nvim_chan_send(term, '\027]8;;https://example.com\027\\')
    914          vim.api.nvim_buf_delete(0, { force = true, unload = true })
    915        end)
    916        assert_alive()
    917      end)
    918    end)
    919  end)
    920 
    921  it('no heap-buffer-overflow when using jobstart("echo",{term=true}) #3161', function()
    922    local testfilename = 'Xtestfile-functional-terminal-buffers_spec'
    923    write_file(testfilename, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa')
    924    finally(function()
    925      os.remove(testfilename)
    926    end)
    927    feed(':edit ' .. testfilename .. '<CR>')
    928    -- Move cursor away from the beginning of the line
    929    feed('$')
    930    -- Let jobstart(…,{term=true}) modify the buffer
    931    fn.jobstart('echo', { term = true })
    932    assert_alive()
    933    command('bdelete!')
    934  end)
    935 
    936  it('no heap-buffer-overflow when sending long line with nowrap #11548', function()
    937    command('set nowrap | autocmd TermOpen * startinsert')
    938    feed(':call feedkeys("4000ai\\<esc>:terminal!\\<cr>")<CR>')
    939    assert_alive()
    940  end)
    941 
    942  it('truncates the size of grapheme clusters', function()
    943    local chan = api.nvim_open_term(0, {})
    944    local composing = ('a̳'):sub(2)
    945    api.nvim_chan_send(chan, 'a' .. composing:rep(20))
    946    retry(nil, nil, function()
    947      eq('a' .. composing:rep(14), api.nvim_get_current_line())
    948    end)
    949  end)
    950 
    951  it('handles extended grapheme clusters', function()
    952    local screen = Screen.new(50, 7)
    953    feed 'i'
    954    local chan = api.nvim_open_term(0, {})
    955    api.nvim_chan_send(chan, '🏴‍☠️ yarrr')
    956    screen:expect([[
    957      🏴‍☠️ yarrr^                                          |
    958                                                        |*5
    959      {5:-- TERMINAL --}                                    |
    960    ]])
    961    eq('🏴‍☠️ yarrr', api.nvim_get_current_line())
    962  end)
    963 
    964  it('handles split UTF-8 sequences #16245', function()
    965    local screen = Screen.new(50, 10)
    966    fn.jobstart({ testprg('shell-test'), 'UTF-8' }, { term = true })
    967    screen:expect([[
    968      ^å                                                 |
    969      ref: å̲                                            |
    970      1: å̲                                              |
    971      2: å̲                                              |
    972      3: å̲                                              |
    973                                                        |
    974      [Process exited 0]                                |
    975                                                        |*3
    976    ]])
    977    -- Test with Unicode char at right edge using a 4-wide terminal
    978    command('bwipe! | set laststatus=0 | 4vnew')
    979    fn.jobstart({ testprg('shell-test'), 'UTF-8' }, { term = true })
    980    screen:expect([[
    981      ^å                                                |
    982      ref:│{1:~                                            }|
    983       å̲  {1:~                                            }|
    984      1: å̲│{1:~                                            }|
    985      2: å̲│{1:~                                            }|
    986      3: å̲│{1:~                                            }|
    987          {1:~                                            }|
    988      [Pro{1:~                                            }|
    989      cess{1:~                                            }|
    990                                                        |
    991    ]])
    992  end)
    993 
    994  --- @param subcmd 'REP'|'REPFAST'
    995  local function check_term_rep(subcmd, count)
    996    local screen = Screen.new(50, 7)
    997    api.nvim_create_autocmd('TermClose', { command = 'let g:did_termclose = 1' })
    998    fn.jobstart({ testprg('shell-test'), subcmd, count, 'TEST' }, { term = true })
    999    retry(nil, nil, function()
   1000      eq(1, api.nvim_get_var('did_termclose'))
   1001    end)
   1002    feed('i')
   1003    screen:expect(([[
   1004      %d: TEST{MATCH: +}|
   1005      %d: TEST{MATCH: +}|
   1006      %d: TEST{MATCH: +}|
   1007      %d: TEST{MATCH: +}|
   1008                                                        |
   1009      [Process exited 0]^                                |
   1010      {5:-- TERMINAL --}                                    |
   1011    ]]):format(count - 4, count - 3, count - 2, count - 1))
   1012    local lines = api.nvim_buf_get_lines(0, 0, -1, true)
   1013    for i = 1, count do
   1014      eq(('%d: TEST'):format(i - 1), lines[i])
   1015    end
   1016  end
   1017 
   1018  it('does not drop data when job exits immediately after output #3030', function()
   1019    api.nvim_set_option_value('scrollback', 30000, {})
   1020    check_term_rep('REPFAST', 20000)
   1021  end)
   1022 
   1023  it('does not drop data when autocommands poll for events #37559', function()
   1024    api.nvim_set_option_value('scrollback', 30000, {})
   1025    api.nvim_create_autocmd('BufFilePre', { command = 'sleep 50m', nested = true })
   1026    api.nvim_create_autocmd('BufFilePost', { command = 'sleep 50m', nested = true })
   1027    api.nvim_create_autocmd('TermOpen', { command = 'sleep 50m', nested = true })
   1028    -- REP pauses 1 ms every 100 lines, so each autocommand processes some output.
   1029    check_term_rep('REP', 20000)
   1030  end)
   1031 
   1032  describe('scrollback is correct if all output is drained by', function()
   1033    for _, event in ipairs({ 'BufFilePre', 'BufFilePost', 'TermOpen' }) do
   1034      describe(('%s autocommand that lasts for'):format(event), function()
   1035        for _, delay in ipairs({ 5, 15, 25 }) do
   1036          -- Terminal refresh delay is 10 ms.
   1037          it(('%.1f * terminal refresh delay'):format(delay / 10), function()
   1038            local cmd = ('sleep %dm'):format(delay)
   1039            api.nvim_create_autocmd(event, { command = cmd, nested = true })
   1040            check_term_rep('REPFAST', 200)
   1041          end)
   1042        end
   1043      end)
   1044    end
   1045  end)
   1046 
   1047  it('handles unprintable chars', function()
   1048    local screen = Screen.new(50, 7)
   1049    feed 'i'
   1050    local chan = api.nvim_open_term(0, {})
   1051    api.nvim_chan_send(chan, '\239\187\191') -- '\xef\xbb\xbf'
   1052    screen:expect([[
   1053      {18:<feff>}^                                            |
   1054                                                        |*5
   1055      {5:-- TERMINAL --}                                    |
   1056    ]])
   1057    eq('\239\187\191', api.nvim_get_current_line())
   1058  end)
   1059 
   1060  it("handles bell respecting 'belloff' and 'visualbell'", function()
   1061    local screen = Screen.new(50, 7)
   1062    local chan = api.nvim_open_term(0, {})
   1063 
   1064    command('set belloff=')
   1065    api.nvim_chan_send(chan, '\a')
   1066    screen:expect(function()
   1067      eq({ true, false }, { screen.bell, screen.visual_bell })
   1068    end)
   1069    screen.bell = false
   1070 
   1071    command('set visualbell')
   1072    api.nvim_chan_send(chan, '\a')
   1073    screen:expect(function()
   1074      eq({ false, true }, { screen.bell, screen.visual_bell })
   1075    end)
   1076    screen.visual_bell = false
   1077 
   1078    command('set belloff=term')
   1079    api.nvim_chan_send(chan, '\a')
   1080    screen:expect({
   1081      condition = function()
   1082        eq({ false, false }, { screen.bell, screen.visual_bell })
   1083      end,
   1084      unchanged = true,
   1085    })
   1086 
   1087    command('set belloff=all')
   1088    api.nvim_chan_send(chan, '\a')
   1089    screen:expect({
   1090      condition = function()
   1091        eq({ false, false }, { screen.bell, screen.visual_bell })
   1092      end,
   1093      unchanged = true,
   1094    })
   1095  end)
   1096 
   1097  it('does not wipeout unrelated buffer after channel closes', function()
   1098    local screen = Screen.new(50, 7)
   1099    screen:set_default_attr_ids({
   1100      [1] = { foreground = Screen.colors.Blue1, bold = true },
   1101      [2] = { reverse = true },
   1102      [31] = { background = Screen.colors.DarkGreen, foreground = Screen.colors.White, bold = true },
   1103    })
   1104 
   1105    local old_buf = api.nvim_get_current_buf()
   1106    command('new')
   1107    fn.chanclose(api.nvim_open_term(0, {}))
   1108    local term_buf = api.nvim_get_current_buf()
   1109    screen:expect([[
   1110      ^                                                  |
   1111      [Terminal closed]                                 |
   1112      {31:[Scratch] [-]                                     }|
   1113                                                        |
   1114      {1:~                                                 }|
   1115      {2:[No Name]                                         }|
   1116                                                        |
   1117    ]])
   1118 
   1119    -- Autocommand should not result in the wrong buffer being wiped out.
   1120    command('autocmd TermLeave * ++once wincmd p')
   1121    feed('ii')
   1122    screen:expect([[
   1123      ^                                                  |
   1124      {1:~                                                 }|*5
   1125                                                        |
   1126    ]])
   1127    eq(old_buf, api.nvim_get_current_buf())
   1128    eq(false, api.nvim_buf_is_valid(term_buf))
   1129 
   1130    term_buf = api.nvim_get_current_buf()
   1131    fn.chanclose(api.nvim_open_term(term_buf, {}))
   1132    screen:expect([[
   1133      ^                                                  |
   1134      [Terminal closed]                                 |
   1135                                                        |*5
   1136    ]])
   1137 
   1138    -- Autocommand should not result in a heap UAF if it frees the terminal prematurely.
   1139    command('autocmd TermLeave * ++once bwipeout!')
   1140    feed('ii')
   1141    screen:expect([[
   1142      ^                                                  |
   1143      {1:~                                                 }|*5
   1144                                                        |
   1145    ]])
   1146    eq(false, api.nvim_buf_is_valid(term_buf))
   1147  end)
   1148 
   1149  local enew_screen = [[
   1150    ^                                                  |
   1151    {1:~                                                 }|*5
   1152                                                      |
   1153  ]]
   1154 
   1155  local function test_enew_in_buf_with_running_term(env)
   1156    describe('editing a new file', function()
   1157      it('hides terminal buffer ignoring bufhidden=wipe', function()
   1158        local old_snapshot = env.screen:get_snapshot()
   1159        command('setlocal bufhidden=wipe')
   1160        command('enew')
   1161        neq(env.buf, api.nvim_get_current_buf())
   1162        env.screen:expect(enew_screen)
   1163        feed('<C-^>')
   1164        eq(env.buf, api.nvim_get_current_buf())
   1165        env.screen:expect(old_snapshot)
   1166      end)
   1167    end)
   1168  end
   1169 
   1170  local function test_open_term_in_buf_with_running_term(env)
   1171    describe('does not allow opening another terminal', function()
   1172      it('with jobstart() in same buffer', function()
   1173        eq(
   1174          ('Vim:Terminal already connected to buffer %d'):format(env.buf),
   1175          pcall_err(fn.jobstart, { testprg('tty-test') }, { term = true })
   1176        )
   1177        env.screen:expect_unchanged()
   1178      end)
   1179 
   1180      it('with nvim_open_term() in same buffer', function()
   1181        eq(
   1182          ('Terminal already connected to buffer %d'):format(env.buf),
   1183          pcall_err(api.nvim_open_term, env.buf, {})
   1184        )
   1185        env.screen:expect_unchanged()
   1186      end)
   1187    end)
   1188  end
   1189 
   1190  describe('with running terminal job', function()
   1191    local env = {}
   1192 
   1193    before_each(function()
   1194      env.screen = Screen.new(50, 7)
   1195      fn.jobstart({ testprg('tty-test') }, { term = true })
   1196      env.screen:expect([[
   1197        ^tty ready                                         |
   1198                                                          |*6
   1199      ]])
   1200      env.buf = api.nvim_get_current_buf()
   1201      api.nvim_set_option_value('modified', false, { buf = env.buf })
   1202    end)
   1203 
   1204    test_enew_in_buf_with_running_term(env)
   1205    test_open_term_in_buf_with_running_term(env)
   1206  end)
   1207 
   1208  describe('with open nvim_open_term() channel', function()
   1209    local env = {}
   1210 
   1211    before_each(function()
   1212      env.screen = Screen.new(50, 7)
   1213      local chan = api.nvim_open_term(0, {})
   1214      api.nvim_chan_send(chan, 'TEST')
   1215      env.screen:expect([[
   1216        ^TEST                                              |
   1217                                                          |*6
   1218      ]])
   1219      env.buf = api.nvim_get_current_buf()
   1220      api.nvim_set_option_value('modified', false, { buf = env.buf })
   1221    end)
   1222 
   1223    test_enew_in_buf_with_running_term(env)
   1224    test_open_term_in_buf_with_running_term(env)
   1225  end)
   1226 
   1227  local function test_enew_in_buf_with_finished_term(env)
   1228    describe('editing a new file', function()
   1229      it('hides terminal buffer with bufhidden=hide', function()
   1230        local old_snapshot = env.screen:get_snapshot()
   1231        command('setlocal bufhidden=hide')
   1232        command('enew')
   1233        neq(env.buf, api.nvim_get_current_buf())
   1234        env.screen:expect(enew_screen)
   1235        feed('<C-^>')
   1236        eq(env.buf, api.nvim_get_current_buf())
   1237        env.screen:expect(old_snapshot)
   1238      end)
   1239 
   1240      it('wipes terminal buffer with bufhidden=wipe', function()
   1241        command('setlocal bufhidden=wipe')
   1242        command('enew')
   1243        neq(env.buf, api.nvim_get_current_buf())
   1244        eq(false, api.nvim_buf_is_valid(env.buf))
   1245        env.screen:expect(enew_screen)
   1246        feed('<C-^>')
   1247        env.screen:expect([[
   1248          ^                                                  |
   1249          {1:~                                                 }|*5
   1250          {9:E23: No alternate file}                            |
   1251        ]])
   1252      end)
   1253    end)
   1254  end
   1255 
   1256  local function test_open_term_in_buf_with_finished_term(env)
   1257    describe('does not leak memory when opening another terminal', function()
   1258      describe('with jobstart() in same buffer', function()
   1259        it('in Normal mode', function()
   1260          fn.jobstart({ testprg('tty-test') }, { term = true })
   1261          env.screen:expect([[
   1262            ^tty ready                                         |
   1263                                                              |*6
   1264          ]])
   1265        end)
   1266 
   1267        it('in Terminal mode', function()
   1268          feed('i')
   1269          eq({ blocking = false, mode = 't' }, api.nvim_get_mode())
   1270          fn.jobstart({ testprg('tty-test') }, { term = true })
   1271          env.screen:expect([[
   1272            tty ready                                         |
   1273            ^                                                  |
   1274                                                              |*4
   1275            {5:-- TERMINAL --}                                    |
   1276          ]])
   1277        end)
   1278      end)
   1279 
   1280      describe('with nvim_open_term() in same buffer', function()
   1281        it('in Normal mode', function()
   1282          local chan = api.nvim_open_term(env.buf, {})
   1283          api.nvim_chan_send(chan, 'OTHER')
   1284          env.screen:expect([[
   1285            ^OTHER                                             |
   1286                                                              |*6
   1287          ]])
   1288        end)
   1289 
   1290        it('in Terminal mode', function()
   1291          feed('i')
   1292          eq({ blocking = false, mode = 't' }, api.nvim_get_mode())
   1293          local chan = api.nvim_open_term(env.buf, {})
   1294          api.nvim_chan_send(chan, 'OTHER')
   1295          env.screen:expect([[
   1296            OTHER^                                             |
   1297                                                              |*5
   1298            {5:-- TERMINAL --}                                    |
   1299          ]])
   1300        end)
   1301      end)
   1302    end)
   1303  end
   1304 
   1305  describe('with exited terminal job', function()
   1306    local env = {}
   1307 
   1308    before_each(function()
   1309      env.screen = Screen.new(50, 7)
   1310      fn.jobstart({ testprg('shell-test') }, { term = true })
   1311      env.screen:expect([[
   1312        ^ready $                                           |
   1313        [Process exited 0]                                |
   1314                                                          |*5
   1315      ]])
   1316      env.buf = api.nvim_get_current_buf()
   1317      api.nvim_set_option_value('modified', false, { buf = env.buf })
   1318    end)
   1319 
   1320    test_enew_in_buf_with_finished_term(env)
   1321    test_open_term_in_buf_with_finished_term(env)
   1322  end)
   1323 
   1324  describe('with closed nvim_open_term() channel', function()
   1325    local env = {}
   1326 
   1327    before_each(function()
   1328      env.screen = Screen.new(50, 7)
   1329      local chan = api.nvim_open_term(0, {})
   1330      api.nvim_chan_send(chan, 'TEST')
   1331      fn.chanclose(chan)
   1332      env.screen:expect([[
   1333        ^TEST                                              |
   1334        [Terminal closed]                                 |
   1335                                                          |*5
   1336      ]])
   1337      env.buf = api.nvim_get_current_buf()
   1338      api.nvim_set_option_value('modified', false, { buf = env.buf })
   1339    end)
   1340 
   1341    test_enew_in_buf_with_finished_term(env)
   1342    test_open_term_in_buf_with_finished_term(env)
   1343  end)
   1344 
   1345  it('with nvim_open_term() channel and only 1 line is not reused by :enew', function()
   1346    command('1new')
   1347    local oldbuf = api.nvim_get_current_buf()
   1348    api.nvim_open_term(oldbuf, {})
   1349    eq({ mode = 'nt', blocking = false }, api.nvim_get_mode())
   1350    feed('i')
   1351    eq({ mode = 't', blocking = false }, api.nvim_get_mode())
   1352    feed([[<C-\><C-N>]])
   1353    eq({ mode = 'nt', blocking = false }, api.nvim_get_mode())
   1354 
   1355    command('enew')
   1356    neq(oldbuf, api.nvim_get_current_buf())
   1357    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
   1358    feed('i')
   1359    eq({ mode = 'i', blocking = false }, api.nvim_get_mode())
   1360    feed('<Esc>')
   1361    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
   1362 
   1363    command('buffer #')
   1364    eq(oldbuf, api.nvim_get_current_buf())
   1365    eq({ mode = 'nt', blocking = false }, api.nvim_get_mode())
   1366    feed('i')
   1367    eq({ mode = 't', blocking = false }, api.nvim_get_mode())
   1368    feed([[<C-\><C-N>]])
   1369    eq({ mode = 'nt', blocking = false }, api.nvim_get_mode())
   1370  end)
   1371 
   1372  it('does not allow OptionSet or b:term_title watcher to delete buffer', function()
   1373    local au = api.nvim_create_autocmd('OptionSet', { command = 'bwipeout!' })
   1374    local chan = api.nvim_open_term(0, {})
   1375    matches('^E937: ', api.nvim_get_vvar('errmsg'))
   1376    api.nvim_del_autocmd(au)
   1377    api.nvim_set_vvar('errmsg', '')
   1378 
   1379    api.nvim_chan_send(chan, '\027]2;SOME_TITLE\007')
   1380    eq('SOME_TITLE', api.nvim_buf_get_var(0, 'term_title'))
   1381    command([[call dictwatcheradd(b:, 'term_title', {-> execute('bwipe!')})]])
   1382    api.nvim_chan_send(chan, '\027]2;OTHER_TITLE\007')
   1383    eq('OTHER_TITLE', api.nvim_buf_get_var(0, 'term_title'))
   1384    matches('^E937: ', api.nvim_get_vvar('errmsg'))
   1385  end)
   1386 
   1387  it('using NameBuff in BufFilePre does not interfere with buffer rename', function()
   1388    local oldbuf = api.nvim_get_current_buf()
   1389    n.exec([[
   1390      file Xoldfile
   1391      new Xotherfile
   1392      wincmd w
   1393      let g:BufFilePre_bufs = []
   1394      let g:BufFilePost_bufs = []
   1395      autocmd BufFilePre * call add(g:BufFilePre_bufs, [bufnr(), bufname()])
   1396      autocmd BufFilePost * call add(g:BufFilePost_bufs, [bufnr(), bufname()])
   1397      autocmd BufFilePre,BufFilePost * call execute('ls')
   1398    ]])
   1399    fn.jobstart({ testprg('shell-test') }, { term = true })
   1400    eq({ { oldbuf, 'Xoldfile' } }, api.nvim_get_var('BufFilePre_bufs'))
   1401    local buffilepost_bufs = api.nvim_get_var('BufFilePost_bufs')
   1402    eq(1, #buffilepost_bufs)
   1403    eq(oldbuf, buffilepost_bufs[1][1])
   1404    matches('^term://', buffilepost_bufs[1][2])
   1405  end)
   1406 end)
   1407 
   1408 describe('on_lines does not emit out-of-bounds line indexes when', function()
   1409  before_each(function()
   1410    clear()
   1411    exec_lua([[
   1412      function _G.register_callback(bufnr)
   1413        _G.cb_error = ''
   1414        vim.api.nvim_buf_attach(bufnr, false, {
   1415          on_lines = function(_, bufnr, _, firstline, _, _)
   1416            local status, msg = pcall(vim.api.nvim_buf_get_offset, bufnr, firstline)
   1417            if not status then
   1418              _G.cb_error = msg
   1419            end
   1420          end
   1421        })
   1422      end
   1423    ]])
   1424  end)
   1425 
   1426  it('creating a terminal buffer #16394', function()
   1427    command('autocmd TermOpen * ++once call v:lua.register_callback(str2nr(expand("<abuf>")))')
   1428    command('terminal')
   1429    sleep(500)
   1430    eq('', exec_lua([[return _G.cb_error]]))
   1431  end)
   1432 
   1433  it('deleting a terminal buffer #16394', function()
   1434    command('terminal')
   1435    sleep(500)
   1436    local cb_error = exec_lua([[
   1437      _G.register_callback(0)
   1438      vim.cmd('bdelete!')
   1439      return _G.cb_error
   1440    ]])
   1441    eq('', cb_error)
   1442  end)
   1443 end)
   1444 
   1445 describe('terminal input', function()
   1446  local chan --- @type integer
   1447 
   1448  before_each(function()
   1449    clear()
   1450    chan = exec_lua(function()
   1451      _G.input_data = ''
   1452      return vim.api.nvim_open_term(0, {
   1453        on_input = function(_, _, _, data)
   1454          _G.input_data = _G.input_data .. data
   1455        end,
   1456      })
   1457    end)
   1458    feed('i')
   1459    poke_eventloop()
   1460  end)
   1461 
   1462  it('<C-Space> is sent as NUL byte', function()
   1463    feed('aaa<C-Space>bbb')
   1464    eq('aaa\0bbb', exec_lua([[return _G.input_data]]))
   1465  end)
   1466 
   1467  it('unknown special keys are not sent', function()
   1468    feed('aaa<Help>bbb')
   1469    eq('aaabbb', exec_lua([[return _G.input_data]]))
   1470  end)
   1471 
   1472  it('<Ignore> is no-op', function()
   1473    feed('aaa<Ignore>bbb')
   1474    eq('aaabbb', exec_lua([[return _G.input_data]]))
   1475    eq({ mode = 't', blocking = false }, api.nvim_get_mode())
   1476    feed([[<C-\><Ignore><C-N>]])
   1477    eq({ mode = 'nt', blocking = false }, api.nvim_get_mode())
   1478    feed('v')
   1479    eq({ mode = 'v', blocking = false }, api.nvim_get_mode())
   1480    feed('<Esc>')
   1481    eq({ mode = 'nt', blocking = false }, api.nvim_get_mode())
   1482    feed('i')
   1483    eq({ mode = 't', blocking = false }, api.nvim_get_mode())
   1484    feed([[<C-\><Ignore><C-O>]])
   1485    eq({ mode = 'ntT', blocking = false }, api.nvim_get_mode())
   1486    feed('v')
   1487    eq({ mode = 'v', blocking = false }, api.nvim_get_mode())
   1488    feed('<Esc>')
   1489    eq({ mode = 't', blocking = false }, api.nvim_get_mode())
   1490    fn.chanclose(chan)
   1491    feed('<MouseMove>')
   1492    eq({ mode = 't', blocking = false }, api.nvim_get_mode())
   1493    feed('<Ignore>')
   1494    eq({ mode = 't', blocking = false }, api.nvim_get_mode())
   1495    eq('terminal', api.nvim_get_option_value('buftype', { buf = 0 }))
   1496    feed('<Space>')
   1497    eq({ mode = 'n', blocking = false }, api.nvim_get_mode())
   1498    eq('', api.nvim_get_option_value('buftype', { buf = 0 }))
   1499  end)
   1500 end)
   1501 
   1502 describe('terminal input', function()
   1503  it('sends various special keys with modifiers', function()
   1504    clear()
   1505    local screen = tt.setup_child_nvim({
   1506      '-u',
   1507      'NONE',
   1508      '-i',
   1509      'NONE',
   1510      '--cmd',
   1511      'colorscheme vim',
   1512      '--cmd',
   1513      'set notermguicolors',
   1514      '-c',
   1515      'while 1 | redraw | echo keytrans(getcharstr(-1, #{simplify: 0})) | endwhile',
   1516    })
   1517    screen:expect([[
   1518      ^                                                  |
   1519      {100:~                                                 }|*3
   1520      {3:[No Name]                       0,0-1          All}|
   1521                                                        |
   1522      {5:-- TERMINAL --}                                    |
   1523    ]])
   1524    local keys = {
   1525      '<Tab>',
   1526      '<CR>',
   1527      '<Esc>',
   1528      '<M-Tab>',
   1529      '<M-CR>',
   1530      '<M-Esc>',
   1531      '<BS>',
   1532      '<S-Tab>',
   1533      '<Insert>',
   1534      '<Del>',
   1535      '<PageUp>',
   1536      '<PageDown>',
   1537      '<S-Up>',
   1538      '<C-Up>',
   1539      '<Up>',
   1540      '<S-Down>',
   1541      '<C-Down>',
   1542      '<Down>',
   1543      '<S-Left>',
   1544      '<C-Left>',
   1545      '<Left>',
   1546      '<S-Right>',
   1547      '<C-Right>',
   1548      '<Right>',
   1549      '<S-Home>',
   1550      '<C-Home>',
   1551      '<Home>',
   1552      '<S-End>',
   1553      '<C-End>',
   1554      '<End>',
   1555      '<C-LeftMouse><0,0>',
   1556      '<C-LeftDrag><0,1>',
   1557      '<C-LeftRelease><0,1>',
   1558      '<2-LeftMouse><0,1>',
   1559      '<2-LeftDrag><0,0>',
   1560      '<2-LeftRelease><0,0>',
   1561      '<M-MiddleMouse><0,0>',
   1562      '<M-MiddleDrag><0,1>',
   1563      '<M-MiddleRelease><0,1>',
   1564      '<2-MiddleMouse><0,1>',
   1565      '<2-MiddleDrag><0,0>',
   1566      '<2-MiddleRelease><0,0>',
   1567      '<S-RightMouse><0,0>',
   1568      '<S-RightDrag><0,1>',
   1569      '<S-RightRelease><0,1>',
   1570      '<2-RightMouse><0,1>',
   1571      '<2-RightDrag><0,0>',
   1572      '<2-RightRelease><0,0>',
   1573      '<S-X1Mouse><0,0>',
   1574      '<S-X1Drag><0,1>',
   1575      '<S-X1Release><0,1>',
   1576      '<2-X1Mouse><0,1>',
   1577      '<2-X1Drag><0,0>',
   1578      '<2-X1Release><0,0>',
   1579      '<S-X2Mouse><0,0>',
   1580      '<S-X2Drag><0,1>',
   1581      '<S-X2Release><0,1>',
   1582      '<2-X2Mouse><0,1>',
   1583      '<2-X2Drag><0,0>',
   1584      '<2-X2Release><0,0>',
   1585      '<S-ScrollWheelUp>',
   1586      '<S-ScrollWheelDown>',
   1587      '<ScrollWheelUp>',
   1588      '<ScrollWheelDown>',
   1589      '<S-ScrollWheelLeft>',
   1590      '<S-ScrollWheelRight>',
   1591      '<ScrollWheelLeft>',
   1592      '<ScrollWheelRight>',
   1593    }
   1594    -- FIXME: The escape sequence to enable kitty keyboard mode doesn't work on Windows
   1595    if not is_os('win') then
   1596      table.insert(keys, '<C-I>')
   1597      table.insert(keys, '<C-M>')
   1598      table.insert(keys, '<C-[>')
   1599    end
   1600    for _, key in ipairs(keys) do
   1601      feed(key)
   1602      screen:expect(([[
   1603                                                          |
   1604        {100:~                                                 }|*3
   1605        {3:[No Name]                       0,0-1          All}|
   1606        %s^ {MATCH: *}|
   1607        {5:-- TERMINAL --}                                    |
   1608      ]]):format(key:gsub('<%d+,%d+>$', '')))
   1609    end
   1610  end)
   1611 end)
   1612 
   1613 if is_os('win') then
   1614  describe(':terminal in Windows', function()
   1615    local screen
   1616 
   1617    before_each(function()
   1618      clear()
   1619      command('set modifiable swapfile undolevels=20')
   1620      poke_eventloop()
   1621      local cmd = { 'cmd.exe', '/K', 'PROMPT=$g$s' }
   1622      screen = tt.setup_screen(nil, cmd)
   1623    end)
   1624 
   1625    it('"put" operator sends data normally', function()
   1626      feed('<c-\\><c-n>G')
   1627      local s = ':: tty ready'
   1628      fn.setreg('a', s .. '\n:: appended ' .. s .. '\n\n')
   1629      feed('"ap"ap')
   1630      screen:expect([[
   1631                                                        |
   1632      > :: tty ready                                    |
   1633      > :: appended :: tty ready                        |
   1634      > :: tty ready                                    |
   1635      > :: appended :: tty ready                        |
   1636      ^>                                                 |
   1637                                                        |
   1638      ]])
   1639      -- operator count is also taken into consideration
   1640      feed('3"ap')
   1641      screen:expect([[
   1642      > :: appended :: tty ready                        |
   1643      > :: tty ready                                    |
   1644      > :: appended :: tty ready                        |
   1645      > :: tty ready                                    |
   1646      > :: appended :: tty ready                        |
   1647      ^>                                                 |
   1648                                                        |
   1649      ]])
   1650    end)
   1651 
   1652    it('":put" command sends data normally', function()
   1653      feed('<c-\\><c-n>G')
   1654      local s = ':: tty ready'
   1655      fn.setreg('a', s .. '\n:: appended ' .. s .. '\n\n')
   1656      feed(':put a<CR>')
   1657      screen:expect([[
   1658                                                        |
   1659      > :: tty ready                                    |
   1660      > :: appended :: tty ready                        |
   1661      >                                                 |
   1662                                                        |
   1663      ^                                                  |
   1664      :put a                                            |
   1665      ]])
   1666      -- line argument is only used to move the cursor
   1667      feed(':6put a<CR>')
   1668      screen:expect([[
   1669                                                        |
   1670      > :: tty ready                                    |
   1671      > :: appended :: tty ready                        |
   1672      > :: tty ready                                    |
   1673      > :: appended :: tty ready                        |
   1674      ^>                                                 |
   1675      :6put a                                           |
   1676      ]])
   1677    end)
   1678  end)
   1679 end
   1680 
   1681 describe('termopen() (deprecated alias to `jobstart(…,{term=true})`)', function()
   1682  before_each(clear)
   1683 
   1684  it('disallowed when textlocked and in cmdwin buffer', function()
   1685    command("autocmd TextYankPost <buffer> ++once call termopen('foo')")
   1686    matches(
   1687      'Vim%(call%):E565: Not allowed to change text or change window$',
   1688      pcall_err(command, 'normal! yy')
   1689    )
   1690 
   1691    feed('q:')
   1692    eq(
   1693      'Vim:E11: Invalid in command-line window; <CR> executes, CTRL-C quits',
   1694      pcall_err(fn.termopen, 'bar')
   1695    )
   1696  end)
   1697 end)
   1698 
   1699 describe('jobstart(…,{term=true})', function()
   1700  before_each(clear)
   1701 
   1702  describe('$COLORTERM value', function()
   1703    before_each(function()
   1704      -- Outer value should never be propagated to :terminal
   1705      fn.setenv('COLORTERM', 'wrongvalue')
   1706    end)
   1707 
   1708    local function test_term_colorterm(expected, opts)
   1709      local screen = Screen.new(50, 4)
   1710      fn.jobstart({
   1711        nvim_prog,
   1712        '-u',
   1713        'NONE',
   1714        '-i',
   1715        'NONE',
   1716        '--headless',
   1717        '-c',
   1718        'echo $COLORTERM | quit',
   1719      }, vim.tbl_extend('error', opts, { term = true }))
   1720      screen:expect(([[
   1721        ^%s{MATCH:%%s+}|
   1722        [Process exited 0]                                |
   1723                                                          |*2
   1724      ]]):format(expected))
   1725    end
   1726 
   1727    describe("with 'notermguicolors'", function()
   1728      before_each(function()
   1729        command('set notermguicolors')
   1730      end)
   1731      it('is empty by default', function()
   1732        test_term_colorterm('', {})
   1733      end)
   1734      it('can be overridden', function()
   1735        test_term_colorterm('expectedvalue', { env = { COLORTERM = 'expectedvalue' } })
   1736      end)
   1737    end)
   1738 
   1739    describe("with 'termguicolors'", function()
   1740      before_each(function()
   1741        command('set termguicolors')
   1742      end)
   1743      it('is "truecolor" by default', function()
   1744        test_term_colorterm('truecolor', {})
   1745      end)
   1746      it('can be overridden', function()
   1747        test_term_colorterm('expectedvalue', { env = { COLORTERM = 'expectedvalue' } })
   1748      end)
   1749    end)
   1750  end)
   1751 end)