neovim

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

scrollback_spec.lua (47398B)


      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 clear, eq, neq = n.clear, t.eq, t.neq
      7 local feed, testprg = n.feed, n.testprg
      8 local fn = n.fn
      9 local eval = n.eval
     10 local command = n.command
     11 local poke_eventloop = n.poke_eventloop
     12 local retry = t.retry
     13 local api = n.api
     14 local feed_data = tt.feed_data
     15 local pcall_err = t.pcall_err
     16 local exec_lua = n.exec_lua
     17 local assert_alive = n.assert_alive
     18 local skip = t.skip
     19 local is_os = t.is_os
     20 
     21 local function test_terminal_scrollback(hide_curbuf)
     22  local screen --- @type test.functional.ui.screen
     23  local buf --- @type integer
     24  local chan --- @type integer
     25  local otherbuf --- @type integer
     26  local restore_terminal_mode --- @type boolean?
     27  local save_feed_data = feed_data
     28 
     29  local function may_hide_curbuf()
     30    if hide_curbuf then
     31      eq(nil, restore_terminal_mode)
     32      restore_terminal_mode = vim.startswith(api.nvim_get_mode().mode, 't')
     33      api.nvim_set_current_buf(otherbuf)
     34    end
     35  end
     36 
     37  local function may_restore_curbuf()
     38    if hide_curbuf then
     39      neq(nil, restore_terminal_mode)
     40      eq(buf, fn.bufnr('#'))
     41      feed('<C-^>') -- "view" in 'jumpoptions' applies to this
     42      if restore_terminal_mode then
     43        feed('i')
     44      else
     45        -- Cursor position was restored from wi_mark, not b_last_cursor.
     46        -- Check that b_last_cursor and wi_mark are the same.
     47        --- @type integer[], integer[]
     48        local last_cursor, restored_cursor = unpack(exec_lua(function()
     49          -- Get these two positions on the same RPC call.
     50          return { vim.fn.getpos([['"]]), vim.fn.getpos('.') }
     51        end))
     52        if last_cursor[2] > 0 then
     53          eq(restored_cursor, last_cursor)
     54        else
     55          eq({ 0, 0, 0, 0 }, last_cursor)
     56          eq({ 0, 1, 1, 0 }, restored_cursor)
     57        end
     58      end
     59      restore_terminal_mode = nil
     60    end
     61  end
     62 
     63  setup(function()
     64    feed_data = function(data)
     65      may_hide_curbuf()
     66      api.nvim_chan_send(chan, data)
     67      may_restore_curbuf()
     68    end
     69  end)
     70 
     71  teardown(function()
     72    feed_data = save_feed_data
     73  end)
     74 
     75  --- @param prefix string
     76  --- @param start integer
     77  --- @param stop integer
     78  local function feed_lines(prefix, start, stop)
     79    may_hide_curbuf()
     80    local data = ''
     81    for i = start, stop do
     82      data = data .. prefix .. tostring(i) .. '\n'
     83    end
     84    api.nvim_chan_send(chan, data)
     85    retry(nil, 1000, function()
     86      eq({ prefix .. tostring(stop), '' }, api.nvim_buf_get_lines(buf, -3, -1, true))
     87    end)
     88    may_restore_curbuf()
     89  end
     90 
     91  local function try_resize(width, height)
     92    may_hide_curbuf()
     93    screen:try_resize(width, height)
     94    may_restore_curbuf()
     95  end
     96 
     97  before_each(function()
     98    clear()
     99    command('set nostartofline jumpoptions+=view')
    100    screen = tt.setup_screen(nil, nil, 30)
    101    buf = api.nvim_get_current_buf()
    102    chan = api.nvim_get_option_value('channel', { buf = buf })
    103    otherbuf = hide_curbuf and api.nvim_create_buf(true, false) or nil
    104    restore_terminal_mode = nil
    105  end)
    106 
    107  describe('when the limit is exceeded', function()
    108    before_each(function()
    109      feed_lines('line', 1, 30)
    110      screen:expect([[
    111        line26                        |
    112        line27                        |
    113        line28                        |
    114        line29                        |
    115        line30                        |
    116        ^                              |
    117        {5:-- TERMINAL --}                |
    118      ]])
    119      eq(16, api.nvim_buf_line_count(0))
    120    end)
    121 
    122    it('will delete extra lines at the top', function()
    123      feed('<c-\\><c-n>gg')
    124      screen:expect([[
    125        ^line16                        |
    126        line17                        |
    127        line18                        |
    128        line19                        |
    129        line20                        |
    130        line21                        |
    131                                      |
    132      ]])
    133    end)
    134 
    135    describe('and cursor on non-last row in screen', function()
    136      before_each(function()
    137        feed([[<C-\><C-N>M$]])
    138        fn.setpos("'m", { 0, 13, 4, 0 })
    139        local ns = api.nvim_create_namespace('test')
    140        api.nvim_buf_set_extmark(0, ns, 12, 0, { end_col = 6, hl_group = 'ErrorMsg' })
    141        screen:expect([[
    142          line26                        |
    143          line27                        |
    144          {101:line2^8}                        |
    145          line29                        |
    146          line30                        |
    147                                        |*2
    148        ]])
    149      end)
    150 
    151      it("outputting fewer than 'scrollback' lines", function()
    152        feed_lines('new_line', 1, 6)
    153        screen:expect([[
    154          line26                        |
    155          line27                        |
    156          {101:line2^8}                        |
    157          line29                        |
    158          line30                        |
    159          new_line1                     |
    160                                        |
    161        ]])
    162        eq({ 0, 7, 4, 0 }, fn.getpos("'m"))
    163        eq({ 0, 7, 6, 0 }, fn.getpos('.'))
    164      end)
    165 
    166      it("outputting more than 'scrollback' lines", function()
    167        feed_lines('new_line', 1, 11)
    168        screen:expect([[
    169          line27                        |
    170          {101:line2^8}                        |
    171          line29                        |
    172          line30                        |
    173          new_line1                     |
    174          new_line2                     |
    175                                        |
    176        ]])
    177        eq({ 0, 2, 4, 0 }, fn.getpos("'m"))
    178        eq({ 0, 2, 6, 0 }, fn.getpos('.'))
    179      end)
    180 
    181      it('outputting more lines than whole buffer', function()
    182        feed_lines('new_line', 1, 20)
    183        screen:expect([[
    184          ^new_line6                     |
    185          new_line7                     |
    186          new_line8                     |
    187          new_line9                     |
    188          new_line10                    |
    189          new_line11                    |
    190                                        |
    191        ]])
    192        eq({ 0, 0, 0, 0 }, fn.getpos("'m"))
    193        eq({ 0, 1, 1, 0 }, fn.getpos('.'))
    194      end)
    195 
    196      it('clearing scrollback with ED 3', function()
    197        feed_data('\027[3J')
    198        screen:expect_unchanged(hide_curbuf)
    199        eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    200        eq({ 0, 3, 6, 0 }, fn.getpos('.'))
    201        feed('gg')
    202        screen:expect([[
    203          line2^6                        |
    204          line27                        |
    205          {101:line28}                        |
    206          line29                        |
    207          line30                        |
    208                                        |*2
    209        ]])
    210      end)
    211 
    212      it('clearing scrollback with ED 3 and outputting lines', function()
    213        feed_data('\027[3J' .. 'new_line1\nnew_line2\nnew_line3')
    214        screen:expect([[
    215          line26                        |
    216          line27                        |
    217          {101:line2^8}                        |
    218          line29                        |
    219          line30                        |
    220          new_line1                     |
    221                                        |
    222        ]])
    223        eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    224        eq({ 0, 3, 6, 0 }, fn.getpos('.'))
    225      end)
    226 
    227      it('clearing scrollback with ED 3 between outputting lines', function()
    228        skip(is_os('win'), 'FIXME: wrong behavior on Windows, ConPTY bug?')
    229        feed_data('line31\nline32\n' .. '\027[3J' .. 'new_line1\nnew_line2')
    230        screen:expect([[
    231          {101:line2^8}                        |
    232          line29                        |
    233          line30                        |
    234          line31                        |
    235          line32                        |
    236          new_line1                     |
    237                                        |
    238        ]])
    239        eq({ 0, 1, 4, 0 }, fn.getpos("'m"))
    240        eq({ 0, 1, 6, 0 }, fn.getpos('.'))
    241      end)
    242    end)
    243 
    244    describe('and cursor on scrollback row #12651', function()
    245      before_each(function()
    246        feed([[<C-\><C-N>Hk$]])
    247        fn.setpos("'m", { 0, 10, 4, 0 })
    248        local ns = api.nvim_create_namespace('test')
    249        api.nvim_buf_set_extmark(0, ns, 9, 0, { end_col = 6, hl_group = 'ErrorMsg' })
    250        screen:expect([[
    251          {101:line2^5}                        |
    252          line26                        |
    253          line27                        |
    254          line28                        |
    255          line29                        |
    256          line30                        |
    257                                        |
    258        ]])
    259      end)
    260 
    261      it("outputting fewer than 'scrollback' lines", function()
    262        feed_lines('new_line', 1, 6)
    263        screen:expect_unchanged(hide_curbuf)
    264        eq({ 0, 4, 4, 0 }, fn.getpos("'m"))
    265        eq({ 0, 4, 6, 0 }, fn.getpos('.'))
    266      end)
    267 
    268      it("outputting more than 'scrollback' lines", function()
    269        feed_lines('new_line', 1, 11)
    270        screen:expect([[
    271          ^line27                        |
    272          line28                        |
    273          line29                        |
    274          line30                        |
    275          new_line1                     |
    276          new_line2                     |
    277                                        |
    278        ]])
    279        eq({ 0, 0, 0, 0 }, fn.getpos("'m"))
    280        eq({ 0, 1, 1, 0 }, fn.getpos('.'))
    281      end)
    282    end)
    283 
    284    it('changing window height does not duplicate lines', function()
    285      -- XXX: Can't test this reliably on Windows unless the cursor is _moved_
    286      --      by the resize. http://docs.libuv.org/en/v1.x/signal.html
    287      --      See also: https://github.com/rprichard/winpty/issues/110
    288      skip(is_os('win'))
    289      try_resize(screen._width, screen._height + 4)
    290      screen:expect([[
    291        line23                        |
    292        line24                        |
    293        line25                        |
    294        line26                        |
    295        line27                        |
    296        line28                        |
    297        line29                        |
    298        line30                        |
    299        rows: 10, cols: 30            |
    300        ^                              |
    301        {5:-- TERMINAL --}                |
    302      ]])
    303      eq(17, api.nvim_buf_line_count(0))
    304      try_resize(screen._width, screen._height - 2)
    305      screen:expect([[
    306        line26                        |
    307        line27                        |
    308        line28                        |
    309        line29                        |
    310        line30                        |
    311        rows: 10, cols: 30            |
    312        rows: 8, cols: 30             |
    313        ^                              |
    314        {5:-- TERMINAL --}                |
    315      ]])
    316      eq(18, api.nvim_buf_line_count(0))
    317      try_resize(screen._width, screen._height - 3)
    318      screen:expect([[
    319        line30                        |
    320        rows: 10, cols: 30            |
    321        rows: 8, cols: 30             |
    322        rows: 5, cols: 30             |
    323        ^                              |
    324        {5:-- TERMINAL --}                |
    325      ]])
    326      eq(15, api.nvim_buf_line_count(0))
    327      try_resize(screen._width, screen._height + 3)
    328      screen:expect([[
    329        line28                        |
    330        line29                        |
    331        line30                        |
    332        rows: 10, cols: 30            |
    333        rows: 8, cols: 30             |
    334        rows: 5, cols: 30             |
    335        rows: 8, cols: 30             |
    336        ^                              |
    337        {5:-- TERMINAL --}                |
    338      ]])
    339      eq(16, api.nvim_buf_line_count(0))
    340      feed([[<C-\><C-N>8<C-Y>]])
    341      screen:expect([[
    342        line20                        |
    343        line21                        |
    344        line22                        |
    345        line23                        |
    346        line24                        |
    347        line25                        |
    348        line26                        |
    349        ^line27                        |
    350                                      |
    351      ]])
    352    end)
    353  end)
    354 
    355  describe('with cursor at last row', function()
    356    before_each(function()
    357      feed_lines('line', 1, 4)
    358      screen:expect([[
    359        tty ready                     |
    360        line1                         |
    361        line2                         |
    362        line3                         |
    363        line4                         |
    364        ^                              |
    365        {5:-- TERMINAL --}                |
    366      ]])
    367      fn.setpos("'m", { 0, 3, 4, 0 })
    368      local ns = api.nvim_create_namespace('test')
    369      api.nvim_buf_set_extmark(0, ns, 2, 0, { end_col = 5, hl_group = 'ErrorMsg' })
    370      screen:expect([[
    371        tty ready                     |
    372        line1                         |
    373        {101:line2}                         |
    374        line3                         |
    375        line4                         |
    376        ^                              |
    377        {5:-- TERMINAL --}                |
    378      ]])
    379    end)
    380 
    381    it("outputting more than 'scrollback' lines in Normal mode", function()
    382      feed([[<C-\><C-N>]])
    383      feed_lines('new_line', 1, 11)
    384      screen:expect([[
    385        new_line7                     |
    386        new_line8                     |
    387        new_line9                     |
    388        new_line10                    |
    389        new_line11                    |
    390        ^                              |
    391                                      |
    392      ]])
    393      feed('gg')
    394      screen:expect([[
    395        ^line1                         |
    396        {101:line2}                         |
    397        line3                         |
    398        line4                         |
    399        new_line1                     |
    400        new_line2                     |
    401                                      |
    402      ]])
    403      eq({ 0, 2, 4, 0 }, fn.getpos("'m"))
    404      feed('G')
    405      feed_lines('new_line', 12, 31)
    406      screen:expect([[
    407        new_line27                    |
    408        new_line28                    |
    409        new_line29                    |
    410        new_line30                    |
    411        new_line31                    |
    412        ^                              |
    413                                      |
    414      ]])
    415      feed('gg')
    416      screen:expect([[
    417        ^new_line17                    |
    418        new_line18                    |
    419        new_line19                    |
    420        new_line20                    |
    421        new_line21                    |
    422        new_line22                    |
    423                                      |
    424      ]])
    425      eq({ 0, 0, 0, 0 }, fn.getpos("'m"))
    426    end)
    427 
    428    it('clearing scrollback with ED 3', function()
    429      -- Clearing empty scrollback and then outputting a line
    430      feed_data('\027[3J' .. 'line5\n')
    431      screen:expect([[
    432        line1                         |
    433        {101:line2}                         |
    434        line3                         |
    435        line4                         |
    436        line5                         |
    437        ^                              |
    438        {5:-- TERMINAL --}                |
    439      ]])
    440      eq(7, api.nvim_buf_line_count(0))
    441      eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    442      -- Clearing 1 line of scrollback
    443      feed_data('\027[3J')
    444      screen:expect_unchanged(hide_curbuf)
    445      eq(6, api.nvim_buf_line_count(0))
    446      eq({ 0, 2, 4, 0 }, fn.getpos("'m"))
    447      -- Outputting a line
    448      feed_data('line6\n')
    449      screen:expect([[
    450        {101:line2}                         |
    451        line3                         |
    452        line4                         |
    453        line5                         |
    454        line6                         |
    455        ^                              |
    456        {5:-- TERMINAL --}                |
    457      ]])
    458      eq(7, api.nvim_buf_line_count(0))
    459      eq({ 0, 2, 4, 0 }, fn.getpos("'m"))
    460      -- Clearing 1 line of scrollback and then outputting a line
    461      feed_data('\027[3J' .. 'line7\n')
    462      screen:expect([[
    463        line3                         |
    464        line4                         |
    465        line5                         |
    466        line6                         |
    467        line7                         |
    468        ^                              |
    469        {5:-- TERMINAL --}                |
    470      ]])
    471      eq(7, api.nvim_buf_line_count(0))
    472      eq({ 0, 1, 4, 0 }, fn.getpos("'m"))
    473      -- Check first line of buffer in Normal mode
    474      feed([[<C-\><C-N>gg]])
    475      screen:expect([[
    476        {101:^line2}                         |
    477        line3                         |
    478        line4                         |
    479        line5                         |
    480        line6                         |
    481        line7                         |
    482                                      |
    483      ]])
    484      feed('G')
    485      -- Outputting lines and then clearing scrollback
    486      skip(is_os('win'), 'FIXME: wrong behavior on Windows, ConPTY bug?')
    487      feed_data('line8\nline9\n' .. '\027[3J')
    488      screen:expect([[
    489        line5                         |
    490        line6                         |
    491        line7                         |
    492        line8                         |
    493        line9                         |
    494        ^                              |
    495                                      |
    496      ]])
    497      eq(6, api.nvim_buf_line_count(0))
    498      eq({ 0, 0, 0, 0 }, fn.getpos("'m"))
    499    end)
    500 
    501    describe('and 1 line is printed', function()
    502      before_each(function()
    503        feed_lines('line', 5, 5)
    504      end)
    505 
    506      it('will hide the top line', function()
    507        screen:expect([[
    508          line1                         |
    509          {101:line2}                         |
    510          line3                         |
    511          line4                         |
    512          line5                         |
    513          ^                              |
    514          {5:-- TERMINAL --}                |
    515        ]])
    516        eq(7, api.nvim_buf_line_count(0))
    517        eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    518      end)
    519 
    520      describe('and then 3 more lines are printed', function()
    521        before_each(function()
    522          feed_lines('line', 6, 8)
    523        end)
    524 
    525        it('will hide the top 4 lines', function()
    526          screen:expect([[
    527            line4                         |
    528            line5                         |
    529            line6                         |
    530            line7                         |
    531            line8                         |
    532            ^                              |
    533            {5:-- TERMINAL --}                |
    534          ]])
    535          eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    536 
    537          feed('<c-\\><c-n>6k')
    538          screen:expect([[
    539            ^line3                         |
    540            line4                         |
    541            line5                         |
    542            line6                         |
    543            line7                         |
    544            line8                         |
    545                                          |
    546          ]])
    547 
    548          feed('gg')
    549          screen:expect([[
    550            ^tty ready                     |
    551            line1                         |
    552            {101:line2}                         |
    553            line3                         |
    554            line4                         |
    555            line5                         |
    556                                          |
    557          ]])
    558 
    559          feed('G')
    560          screen:expect([[
    561            line4                         |
    562            line5                         |
    563            line6                         |
    564            line7                         |
    565            line8                         |
    566            ^                              |
    567                                          |
    568          ]])
    569        end)
    570      end)
    571    end)
    572 
    573    describe('and height decreased by 1', function()
    574      local function will_hide_top_line()
    575        feed([[<C-\><C-N>]])
    576        try_resize(screen._width - 2, screen._height - 1)
    577        screen:expect([[
    578          {101:line2}                       |
    579          line3                       |
    580          line4                       |
    581          rows: 5, cols: 28           |
    582          ^                            |
    583                                      |
    584        ]])
    585        eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    586      end
    587 
    588      it('will hide top line', will_hide_top_line)
    589 
    590      describe('and then decreased by 2', function()
    591        before_each(function()
    592          will_hide_top_line()
    593          try_resize(screen._width - 2, screen._height - 2)
    594        end)
    595 
    596        it('will hide the top 3 lines', function()
    597          screen:expect([[
    598            rows: 5, cols: 28         |
    599            rows: 3, cols: 26         |
    600            ^                          |
    601                                      |
    602          ]])
    603          eq(8, api.nvim_buf_line_count(0))
    604          eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    605          feed('3k')
    606          screen:expect([[
    607            ^line4                     |
    608            rows: 5, cols: 28         |
    609            rows: 3, cols: 26         |
    610                                      |
    611          ]])
    612          feed('gg')
    613          screen:expect([[
    614            ^tty ready                 |
    615            line1                     |
    616            {101:line2}                     |
    617                                      |
    618          ]])
    619        end)
    620      end)
    621    end)
    622  end)
    623 
    624  describe('with empty lines after the cursor', function()
    625    -- XXX: Can't test this reliably on Windows unless the cursor is _moved_
    626    --      by the resize. http://docs.libuv.org/en/v1.x/signal.html
    627    --      See also: https://github.com/rprichard/winpty/issues/110
    628    if skip(is_os('win')) then
    629      return
    630    end
    631 
    632    describe('and the height is decreased by 2', function()
    633      before_each(function()
    634        try_resize(screen._width, screen._height - 2)
    635      end)
    636 
    637      local function will_delete_last_two_lines()
    638        screen:expect([[
    639          tty ready                     |
    640          rows: 4, cols: 30             |
    641          ^                              |
    642                                        |
    643          {5:-- TERMINAL --}                |
    644        ]])
    645        eq(4, api.nvim_buf_line_count(0))
    646      end
    647 
    648      it('will delete the last two empty lines', will_delete_last_two_lines)
    649 
    650      describe('and then decreased by 1', function()
    651        before_each(function()
    652          will_delete_last_two_lines()
    653          try_resize(screen._width, screen._height - 1)
    654        end)
    655 
    656        it('will delete the last line and hide the first', function()
    657          screen:expect([[
    658            rows: 4, cols: 30             |
    659            rows: 3, cols: 30             |
    660            ^                              |
    661            {5:-- TERMINAL --}                |
    662          ]])
    663          eq(4, api.nvim_buf_line_count(0))
    664          feed('<c-\\><c-n>gg')
    665          screen:expect([[
    666            ^tty ready                     |
    667            rows: 4, cols: 30             |
    668            rows: 3, cols: 30             |
    669                                          |
    670          ]])
    671          feed('a')
    672          screen:expect([[
    673            rows: 4, cols: 30             |
    674            rows: 3, cols: 30             |
    675            ^                              |
    676            {5:-- TERMINAL --}                |
    677          ]])
    678        end)
    679      end)
    680    end)
    681  end)
    682 
    683  describe('with 4 lines hidden in the scrollback', function()
    684    before_each(function()
    685      feed_lines('line', 1, 4)
    686      screen:expect([[
    687        tty ready                     |
    688        line1                         |
    689        line2                         |
    690        line3                         |
    691        line4                         |
    692        ^                              |
    693        {5:-- TERMINAL --}                |
    694      ]])
    695      fn.setpos("'m", { 0, 3, 4, 0 })
    696      local ns = api.nvim_create_namespace('test')
    697      api.nvim_buf_set_extmark(0, ns, 2, 0, { end_col = 5, hl_group = 'ErrorMsg' })
    698      screen:expect([[
    699        tty ready                     |
    700        line1                         |
    701        {101:line2}                         |
    702        line3                         |
    703        line4                         |
    704        ^                              |
    705        {5:-- TERMINAL --}                |
    706      ]])
    707      try_resize(screen._width, screen._height - 3)
    708      screen:expect([[
    709        line4                         |
    710        rows: 3, cols: 30             |
    711        ^                              |
    712        {5:-- TERMINAL --}                |
    713      ]])
    714      eq(7, api.nvim_buf_line_count(0))
    715    end)
    716 
    717    describe('and the height is increased by 1', function()
    718      -- XXX: Can't test this reliably on Windows unless the cursor is _moved_
    719      --      by the resize. http://docs.libuv.org/en/v1.x/signal.html
    720      --      See also: https://github.com/rprichard/winpty/issues/110
    721      if skip(is_os('win')) then
    722        return
    723      end
    724      local function pop_then_push()
    725        try_resize(screen._width, screen._height + 1)
    726        screen:expect([[
    727          line4                         |
    728          rows: 3, cols: 30             |
    729          rows: 4, cols: 30             |
    730          ^                              |
    731          {5:-- TERMINAL --}                |
    732        ]])
    733        eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    734      end
    735 
    736      it('will pop 1 line and then push it back', pop_then_push)
    737 
    738      describe('and then by 3', function()
    739        before_each(function()
    740          pop_then_push()
    741          eq(8, api.nvim_buf_line_count(0))
    742          try_resize(screen._width, screen._height + 3)
    743        end)
    744 
    745        local function pop3_then_push1()
    746          screen:expect([[
    747            {101:line2}                         |
    748            line3                         |
    749            line4                         |
    750            rows: 3, cols: 30             |
    751            rows: 4, cols: 30             |
    752            rows: 7, cols: 30             |
    753            ^                              |
    754            {5:-- TERMINAL --}                |
    755          ]])
    756          eq(9, api.nvim_buf_line_count(0))
    757          eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    758          feed('<c-\\><c-n>gg')
    759          screen:expect([[
    760            ^tty ready                     |
    761            line1                         |
    762            {101:line2}                         |
    763            line3                         |
    764            line4                         |
    765            rows: 3, cols: 30             |
    766            rows: 4, cols: 30             |
    767                                          |
    768          ]])
    769        end
    770 
    771        it('will pop 3 lines and then push one back', pop3_then_push1)
    772 
    773        describe('and then by 4', function()
    774          before_each(function()
    775            pop3_then_push1()
    776            feed('Gi')
    777            try_resize(screen._width, screen._height + 4)
    778          end)
    779 
    780          it('will show all lines and leave a blank one at the end', function()
    781            screen:expect([[
    782              tty ready                     |
    783              line1                         |
    784              {101:line2}                         |
    785              line3                         |
    786              line4                         |
    787              rows: 3, cols: 30             |
    788              rows: 4, cols: 30             |
    789              rows: 7, cols: 30             |
    790              rows: 11, cols: 30            |
    791              ^                              |
    792                                            |
    793              {5:-- TERMINAL --}                |
    794            ]])
    795            -- since there's an empty line after the cursor, the buffer line
    796            -- count equals the terminal screen height
    797            eq(11, api.nvim_buf_line_count(0))
    798            eq({ 0, 3, 4, 0 }, fn.getpos("'m"))
    799          end)
    800        end)
    801      end)
    802    end)
    803  end)
    804 
    805  it('reducing &scrollback deletes extra lines immediately', function()
    806    feed_lines('line', 1, 30)
    807    screen:expect([[
    808      line26                        |
    809      line27                        |
    810      line28                        |
    811      line29                        |
    812      line30                        |
    813      ^                              |
    814      {5:-- TERMINAL --}                |
    815    ]])
    816    local term_height = 6 -- Actual terminal screen height, not the scrollback
    817    -- Initial
    818    local scrollback = api.nvim_get_option_value('scrollback', { buf = buf })
    819    eq(scrollback + term_height, fn.line('$'))
    820    eq(scrollback + term_height, fn.line('.'))
    821    n.fn.setpos("'m", { 0, scrollback + 1, 4, 0 })
    822    local ns = api.nvim_create_namespace('test')
    823    api.nvim_buf_set_extmark(0, ns, scrollback, 0, { end_col = 6, hl_group = 'ErrorMsg' })
    824    screen:expect([[
    825      {101:line26}                        |
    826      line27                        |
    827      line28                        |
    828      line29                        |
    829      line30                        |
    830      ^                              |
    831      {5:-- TERMINAL --}                |
    832    ]])
    833    -- Reduction
    834    scrollback = scrollback - 2
    835    may_hide_curbuf()
    836    api.nvim_set_option_value('scrollback', scrollback, { buf = buf })
    837    may_restore_curbuf()
    838    eq(scrollback + term_height, fn.line('$'))
    839    eq(scrollback + term_height, fn.line('.'))
    840    screen:expect_unchanged(hide_curbuf)
    841    eq({ 0, scrollback + 1, 4, 0 }, n.fn.getpos("'m"))
    842  end)
    843 end
    844 
    845 describe(':terminal scrollback', function()
    846  describe('in current buffer', function()
    847    test_terminal_scrollback(false)
    848  end)
    849 
    850  describe('in hidden buffer', function()
    851    test_terminal_scrollback(true)
    852  end)
    853 end)
    854 
    855 describe(':terminal prints more lines than the screen height and exits', function()
    856  it('will push extra lines to scrollback', function()
    857    clear()
    858    local screen = Screen.new(30, 7, { rgb = false })
    859    screen:add_extra_attr_ids({ [100] = { foreground = 12 } })
    860    command(
    861      ("call jobstart(['%s', '10'], {'term':v:true}) | startinsert"):format(testprg('tty-test'))
    862    )
    863    screen:expect([[
    864      line6                         |
    865      line7                         |
    866      line8                         |
    867      line9                         |
    868                                    |
    869      [Process exited 0]^            |
    870      {5:-- TERMINAL --}                |
    871    ]])
    872    feed('<cr>')
    873    -- closes the buffer correctly after pressing a key
    874    screen:expect([[
    875      ^                              |
    876      {100:~                             }|*5
    877                                    |
    878    ]])
    879  end)
    880 end)
    881 
    882 describe("'scrollback' option", function()
    883  before_each(function()
    884    clear()
    885  end)
    886 
    887  local function set_fake_shell()
    888    api.nvim_set_option_value('shell', string.format('"%s" INTERACT', testprg('shell-test')), {})
    889  end
    890 
    891  local function expect_lines(expected, epsilon)
    892    local ep = epsilon and epsilon or 0
    893    local actual = eval("line('$')")
    894    if expected > actual + ep and expected < actual - ep then
    895      error('expected (+/- ' .. ep .. '): ' .. expected .. ', actual: ' .. tostring(actual))
    896    end
    897  end
    898 
    899  it('set to 0 behaves as 1', function()
    900    local screen
    901    if is_os('win') then
    902      screen = tt.setup_screen(nil, { 'cmd.exe' }, 30)
    903    else
    904      screen = tt.setup_screen(nil, { 'sh' }, 30)
    905    end
    906 
    907    api.nvim_set_option_value('scrollback', 0, {})
    908    feed_data(('%s REP 31 line%s'):format(testprg('shell-test'), is_os('win') and '\r' or '\n'))
    909    screen:expect { any = '30: line                      ' }
    910    retry(nil, nil, function()
    911      expect_lines(7)
    912    end)
    913  end)
    914 
    915  it('deletes lines (only) if necessary', function()
    916    local screen
    917    if is_os('win') then
    918      command([[let $PROMPT='$$']])
    919      screen = tt.setup_screen(nil, { 'cmd.exe' }, 30)
    920    else
    921      command('let $PS1 = "$"')
    922      screen = tt.setup_screen(nil, { 'sh' }, 30)
    923    end
    924 
    925    api.nvim_set_option_value('scrollback', 200, {})
    926 
    927    -- Wait for prompt.
    928    screen:expect { any = '%$' }
    929 
    930    feed_data(('%s REP 31 line%s'):format(testprg('shell-test'), is_os('win') and '\r' or '\n'))
    931    screen:expect { any = '30: line                      ' }
    932 
    933    retry(nil, nil, function()
    934      expect_lines(33, 2)
    935    end)
    936    api.nvim_set_option_value('scrollback', 10, {})
    937    poke_eventloop()
    938    retry(nil, nil, function()
    939      expect_lines(16)
    940    end)
    941    api.nvim_set_option_value('scrollback', 10000, {})
    942    retry(nil, nil, function()
    943      expect_lines(16)
    944    end)
    945    -- Terminal job data is received asynchronously, may happen before the
    946    -- 'scrollback' option is synchronized with the internal sb_buffer.
    947    command('sleep 100m')
    948 
    949    feed_data(('%s REP 41 line%s'):format(testprg('shell-test'), is_os('win') and '\r' or '\n'))
    950    if is_os('win') then
    951      screen:expect([[
    952        37: line                      |
    953        38: line                      |
    954        39: line                      |
    955        40: line                      |
    956                                      |
    957        $^                             |
    958        {5:-- TERMINAL --}                |
    959      ]])
    960    else
    961      screen:expect([[
    962        36: line                      |
    963        37: line                      |
    964        38: line                      |
    965        39: line                      |
    966        40: line                      |
    967        {MATCH:.*}|
    968        {5:-- TERMINAL --}                |
    969      ]])
    970    end
    971    expect_lines(58)
    972 
    973    -- Verify off-screen state
    974    eq((is_os('win') and '36: line' or '35: line'), eval("getline(line('w0') - 1)->trim(' ', 2)"))
    975    eq((is_os('win') and '27: line' or '26: line'), eval("getline(line('w0') - 10)->trim(' ', 2)"))
    976  end)
    977 
    978  it('defaults to 10000 in :terminal buffers', function()
    979    set_fake_shell()
    980    command('terminal')
    981    eq(10000, api.nvim_get_option_value('scrollback', {}))
    982  end)
    983 
    984  it('error if set to invalid value', function()
    985    eq('Vim(set):E474: Invalid argument: scrollback=-2', pcall_err(command, 'set scrollback=-2'))
    986    eq(
    987      'Vim(set):E474: Invalid argument: scrollback=1000001',
    988      pcall_err(command, 'set scrollback=1000001')
    989    )
    990  end)
    991 
    992  it('defaults to -1 on normal buffers', function()
    993    command('new')
    994    eq(-1, api.nvim_get_option_value('scrollback', {}))
    995  end)
    996 
    997  it(':setlocal in a :terminal buffer', function()
    998    set_fake_shell()
    999 
   1000    -- _Global_ scrollback=-1 defaults :terminal to 10_000.
   1001    command('setglobal scrollback=-1')
   1002    command('terminal')
   1003    eq(10000, api.nvim_get_option_value('scrollback', {}))
   1004 
   1005    -- _Local_ scrollback=-1 in :terminal forces the _maximum_.
   1006    command('setlocal scrollback=-1')
   1007    retry(nil, nil, function() -- Fixup happens on refresh, not immediately.
   1008      eq(1000000, api.nvim_get_option_value('scrollback', {}))
   1009    end)
   1010 
   1011    -- _Local_ scrollback=-1 during TermOpen forces the maximum. #9605
   1012    command('setglobal scrollback=-1')
   1013    command('autocmd TermOpen * setlocal scrollback=-1')
   1014    command('terminal')
   1015    eq(1000000, api.nvim_get_option_value('scrollback', {}))
   1016  end)
   1017 
   1018  it(':setlocal in a normal buffer', function()
   1019    command('new')
   1020    -- :setlocal to -1.
   1021    command('setlocal scrollback=-1')
   1022    eq(-1, api.nvim_get_option_value('scrollback', {}))
   1023    -- :setlocal to anything except -1. Currently, this just has no effect.
   1024    command('setlocal scrollback=42')
   1025    eq(42, api.nvim_get_option_value('scrollback', {}))
   1026  end)
   1027 
   1028  it(':set updates local value and global default', function()
   1029    set_fake_shell()
   1030    command('set scrollback=42') -- set global value
   1031    eq(42, api.nvim_get_option_value('scrollback', {}))
   1032    command('terminal')
   1033    eq(42, api.nvim_get_option_value('scrollback', {})) -- inherits global default
   1034    command('setlocal scrollback=99')
   1035    eq(99, api.nvim_get_option_value('scrollback', {}))
   1036    command('set scrollback<') -- reset to global default
   1037    eq(42, api.nvim_get_option_value('scrollback', {}))
   1038    command('setglobal scrollback=734') -- new global default
   1039    eq(42, api.nvim_get_option_value('scrollback', {})) -- local value did not change
   1040    command('terminal')
   1041    eq(734, api.nvim_get_option_value('scrollback', {}))
   1042  end)
   1043 end)
   1044 
   1045 describe('pending scrollback line handling', function()
   1046  local screen
   1047 
   1048  before_each(function()
   1049    clear()
   1050    screen = Screen.new(30, 7)
   1051  end)
   1052 
   1053  it("does not crash after setting 'number' #14891", function()
   1054    exec_lua [[
   1055      local api = vim.api
   1056      local buf = api.nvim_create_buf(true, true)
   1057      local chan = api.nvim_open_term(buf, {})
   1058      vim.wo.number = true
   1059      api.nvim_chan_send(chan, ("a\n"):rep(11) .. "a")
   1060      api.nvim_win_set_buf(0, buf)
   1061    ]]
   1062    screen:expect [[
   1063      {8:  1 }^a                         |
   1064      {8:  2 }a                         |
   1065      {8:  3 }a                         |
   1066      {8:  4 }a                         |
   1067      {8:  5 }a                         |
   1068      {8:  6 }a                         |
   1069                                    |
   1070    ]]
   1071    feed('G')
   1072    screen:expect [[
   1073      {8:  7 }a                         |
   1074      {8:  8 }a                         |
   1075      {8:  9 }a                         |
   1076      {8: 10 }a                         |
   1077      {8: 11 }a                         |
   1078      {8: 12 }^a                         |
   1079                                    |
   1080    ]]
   1081    assert_alive()
   1082  end)
   1083 
   1084  it('does not crash after nvim_buf_call #14891', function()
   1085    exec_lua(
   1086      [[
   1087      local bufnr = vim.api.nvim_create_buf(false, true)
   1088      local args = ...
   1089      vim.api.nvim_buf_call(bufnr, function()
   1090        vim.fn.jobstart(args, { term = true })
   1091      end)
   1092      vim.api.nvim_win_set_buf(0, bufnr)
   1093      vim.cmd('startinsert')
   1094    ]],
   1095      is_os('win') and { 'cmd.exe', '/c', 'for /L %I in (1,1,12) do @echo hi' }
   1096        or { 'printf', ('hi\n'):rep(12) }
   1097    )
   1098    screen:expect [[
   1099      hi                            |*4
   1100                                    |
   1101      [Process exited 0]^            |
   1102      {5:-- TERMINAL --}                |
   1103    ]]
   1104    assert_alive()
   1105  end)
   1106 
   1107  it('does not crash after deleting buffer lines', function()
   1108    local buf = api.nvim_get_current_buf()
   1109    local chan = api.nvim_open_term(buf, {})
   1110    api.nvim_chan_send(chan, ('a\n'):rep(11) .. 'a')
   1111    screen:expect([[
   1112      ^a                             |
   1113      a                             |*5
   1114                                    |
   1115    ]])
   1116    api.nvim_set_option_value('modifiable', true, { buf = buf })
   1117    api.nvim_buf_set_lines(buf, 0, -1, true, {})
   1118    screen:expect([[
   1119      ^                              |
   1120      {1:~                             }|*5
   1121                                    |
   1122    ]])
   1123    api.nvim_chan_send(chan, ('\nb'):rep(11) .. '\n')
   1124    screen:expect([[
   1125      b                             |*5
   1126      ^                              |
   1127                                    |
   1128    ]])
   1129    assert_alive()
   1130  end)
   1131 end)
   1132 
   1133 describe('scrollback is correct', function()
   1134  local screen --- @type test.functional.ui.screen
   1135  local buf --- @type integer
   1136  local win --- @type integer
   1137 
   1138  before_each(function()
   1139    clear()
   1140    screen = Screen.new(30, 7)
   1141    screen:add_extra_attr_ids({
   1142      [100] = { foreground = tonumber('0xe00000'), fg_indexed = true },
   1143      [101] = { foreground = Screen.colors.White, background = Screen.colors.DarkGreen },
   1144      [102] = {
   1145        bold = true,
   1146        foreground = Screen.colors.White,
   1147        background = Screen.colors.DarkGreen,
   1148      },
   1149    })
   1150    api.nvim_buf_set_lines(0, 0, -1, true, { '\027[31mTEST\027[0m 0' })
   1151    feed('yy99pG$<C-V>98kg<C-A>')
   1152    screen:expect([[
   1153      {18:^[}[31mTEST{18:^[}[0m 0             |
   1154      {18:^[}[31mTEST{18:^[}[0m ^1             |
   1155      {18:^[}[31mTEST{18:^[}[0m 2             |
   1156      {18:^[}[31mTEST{18:^[}[0m 3             |
   1157      {18:^[}[31mTEST{18:^[}[0m 4             |
   1158      {18:^[}[31mTEST{18:^[}[0m 5             |
   1159      99 lines changed              |
   1160    ]])
   1161    buf = api.nvim_get_current_buf()
   1162    win = api.nvim_get_current_win()
   1163    command('set winwidth=10 | 10vnew')
   1164  end)
   1165 
   1166  local function check_buffer_lines(start, stop)
   1167    local lines = api.nvim_buf_get_lines(buf, 0, -1, true)
   1168    for i = start, stop do
   1169      eq(('TEST %d'):format(i), lines[i - start + 1])
   1170    end
   1171    eq('', lines[#lines])
   1172    eq(stop - start + 2, #lines)
   1173  end
   1174 
   1175  local function check_common()
   1176    feed('<C-W>lG')
   1177    screen:expect([[
   1178                {100:TEST} 96            |
   1179      {1:~         }{100:TEST} 97            |
   1180      {1:~         }{100:TEST} 98            |
   1181      {1:~         }{100:TEST} 99            |
   1182      {1:~         }^                   |
   1183      {2:[No Name]  }{102:[Scratch] [-]      }|
   1184      99 lines changed              |
   1185    ]])
   1186  end
   1187 
   1188  it('with nvim_open_term() on buffer with fewer lines than scrollback', function()
   1189    exec_lua(function()
   1190      vim.api.nvim_open_term(buf, {})
   1191      vim.api.nvim_win_set_cursor(win, { 3, 0 })
   1192    end)
   1193    screen:expect([[
   1194      ^          {100:TEST} 0             |
   1195      {1:~         }{100:TEST} 1             |
   1196      {1:~         }{100:TEST} 2             |
   1197      {1:~         }{100:TEST} 3             |
   1198      {1:~         }{100:TEST} 4             |
   1199      {3:[No Name]  }{101:[Scratch] [-]      }|
   1200      99 lines changed              |
   1201    ]])
   1202    eq({ 3, 0 }, api.nvim_win_get_cursor(win))
   1203    check_buffer_lines(0, 99)
   1204    check_common()
   1205  end)
   1206 
   1207  it('with nvim_open_term() on buffer with more lines than scrollback', function()
   1208    api.nvim_set_option_value('scrollback', 10, { buf = buf })
   1209    exec_lua(function()
   1210      vim.api.nvim_open_term(buf, {})
   1211      vim.api.nvim_win_set_cursor(win, { 3, 3 })
   1212    end)
   1213    screen:expect([[
   1214      ^          {100:TEST} 86            |
   1215      {1:~         }{100:TEST} 87            |
   1216      {1:~         }{100:TEST} 88            |
   1217      {1:~         }{100:TEST} 89            |
   1218      {1:~         }{100:TEST} 90            |
   1219      {3:[No Name]  }{101:[Scratch] [-]      }|
   1220      99 lines changed              |
   1221    ]])
   1222    eq({ 1, 0 }, api.nvim_win_get_cursor(win))
   1223    check_buffer_lines(86, 99)
   1224    check_common()
   1225  end)
   1226 
   1227  describe('when window height', function()
   1228    before_each(function()
   1229      feed('<C-W>lGV4kdgg')
   1230      screen:try_resize(30, 20)
   1231      command('botright 9new | wincmd p')
   1232      exec_lua(function()
   1233        vim.g.chan = vim.api.nvim_open_term(buf, {})
   1234        vim.cmd('$')
   1235      end)
   1236      screen:expect([[
   1237                  {100:TEST} 88            |
   1238        {1:~         }{100:TEST} 89            |
   1239        {1:~         }{100:TEST} 90            |
   1240        {1:~         }{100:TEST} 91            |
   1241        {1:~         }{100:TEST} 92            |
   1242        {1:~         }{100:TEST} 93            |
   1243        {1:~         }{100:TEST} 94            |
   1244        {1:~         }^                   |
   1245        {2:[No Name]  }{102:[Scratch] [-]      }|
   1246                                      |
   1247        {1:~                             }|*8
   1248        {2:[No Name]                     }|
   1249                                      |
   1250      ]])
   1251      check_buffer_lines(0, 94)
   1252    end)
   1253 
   1254    local send_cmd = 'call chansend(g:chan, @")'
   1255 
   1256    describe('increases in the same refresh cycle as outputting lines', function()
   1257      --- @type string[][]
   1258      local perms = t.concat_tables(
   1259        t.permutations({ 'resize +2', send_cmd }),
   1260        t.permutations({ 'resize +4', 'resize -2', send_cmd }),
   1261        t.permutations({ 'resize +6', 'resize -4', send_cmd })
   1262      )
   1263      assert(#perms == 2 + 6 + 6)
   1264      local screen_final = [[
   1265                  │{100:TEST} 91            |
   1266        {1:~         }│{100:TEST} 92            |
   1267        {1:~         }│{100:TEST} 93            |
   1268        {1:~         }│{100:TEST} 94            |
   1269        {1:~         }│{100:TEST} 95            |
   1270        {1:~         }│{100:TEST} 96            |
   1271        {1:~         }│{100:TEST} 97            |
   1272        {1:~         }│{100:TEST} 98            |
   1273        {1:~         }│{100:TEST} 99            |
   1274        {1:~         }│^                   |
   1275        {2:[No Name]  }{102:[Scratch] [-]      }|
   1276                                      |
   1277        {1:~                             }|*6
   1278        {2:[No Name]                     }|
   1279                                      |
   1280      ]]
   1281 
   1282      for i, perm in ipairs(perms) do
   1283        it(('permutation %d'):format(i), function()
   1284          exec_lua(function()
   1285            for _, cmd in ipairs(perm) do
   1286              vim.cmd(cmd)
   1287            end
   1288          end)
   1289          screen:expect(screen_final)
   1290          check_buffer_lines(0, 99)
   1291        end)
   1292      end
   1293 
   1294      describe('with full scrollback,', function()
   1295        before_each(function()
   1296          api.nvim_set_option_value('scrollback', 6, { buf = buf })
   1297          check_buffer_lines(82, 94)
   1298        end)
   1299 
   1300        it('output first', function()
   1301          command(send_cmd .. ' | resize +2')
   1302          screen:expect(screen_final)
   1303          check_buffer_lines(87, 99)
   1304        end)
   1305 
   1306        it('resize first', function()
   1307          command('resize +2 | ' .. send_cmd)
   1308          screen:expect(screen_final)
   1309          check_buffer_lines(85, 99)
   1310        end)
   1311      end)
   1312    end)
   1313 
   1314    describe('decreases in the same refresh cycle as outputting lines', function()
   1315      --- @type string[][]
   1316      local perms = t.concat_tables(
   1317        t.permutations({ 'resize -2', send_cmd }),
   1318        t.permutations({ 'resize -4', 'resize +2', send_cmd }),
   1319        t.permutations({ 'resize -6', 'resize +4', send_cmd })
   1320      )
   1321      assert(#perms == 2 + 6 + 6)
   1322      local screen_final = [[
   1323                  │{100:TEST} 95            |
   1324        {1:~         }│{100:TEST} 96            |
   1325        {1:~         }│{100:TEST} 97            |
   1326        {1:~         }│{100:TEST} 98            |
   1327        {1:~         }│{100:TEST} 99            |
   1328        {1:~         }│^                   |
   1329        {2:[No Name]  }{102:[Scratch] [-]      }|
   1330                                      |
   1331        {1:~                             }|*10
   1332        {2:[No Name]                     }|
   1333                                      |
   1334      ]]
   1335 
   1336      for i, perm in ipairs(perms) do
   1337        it(('permutation %d'):format(i), function()
   1338          exec_lua(function()
   1339            for _, cmd in ipairs(perm) do
   1340              vim.cmd(cmd)
   1341            end
   1342          end)
   1343          screen:expect(screen_final)
   1344          check_buffer_lines(0, 99)
   1345        end)
   1346      end
   1347    end)
   1348 
   1349    describe("decreases by more than 'scrollback'", function()
   1350      before_each(function()
   1351        api.nvim_set_option_value('scrollback', 4, { buf = buf })
   1352        check_buffer_lines(84, 94)
   1353      end)
   1354 
   1355      local perms = {
   1356        { send_cmd, 'resize -6' },
   1357        { 'resize -6', send_cmd },
   1358        { send_cmd, 'resize +6', 'resize -12' },
   1359        { 'resize +6', send_cmd, 'resize -12' },
   1360        { 'resize +6', 'resize -12', send_cmd },
   1361      }
   1362      local screen_final = [[
   1363                  │{100:TEST} 99            |
   1364        {1:~         }│^                   |
   1365        {2:[No Name]  }{102:[Scratch] [-]      }|
   1366                                      |
   1367        {1:~                             }|*14
   1368        {2:[No Name]                     }|
   1369                                      |
   1370      ]]
   1371 
   1372      for i, perm in ipairs(perms) do
   1373        it(('permutation %d'):format(i), function()
   1374          exec_lua(function()
   1375            for _, cmd in ipairs(perm) do
   1376              vim.cmd(cmd)
   1377            end
   1378          end)
   1379          screen:expect(screen_final)
   1380          check_buffer_lines(95, 99)
   1381        end)
   1382      end
   1383    end)
   1384  end)
   1385 end)