scrollback_spec.lua 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  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. local clear, eq = n.clear, t.eq
  6. local feed, testprg = n.feed, n.testprg
  7. local eval = n.eval
  8. local command = n.command
  9. local poke_eventloop = n.poke_eventloop
  10. local retry = t.retry
  11. local api = n.api
  12. local feed_data = tt.feed_data
  13. local pcall_err = t.pcall_err
  14. local exec_lua = n.exec_lua
  15. local assert_alive = n.assert_alive
  16. local skip = t.skip
  17. local is_os = t.is_os
  18. describe(':terminal scrollback', function()
  19. local screen
  20. before_each(function()
  21. clear()
  22. screen = tt.setup_screen(nil, nil, 30)
  23. end)
  24. describe('when the limit is exceeded', function()
  25. before_each(function()
  26. local lines = {}
  27. for i = 1, 30 do
  28. table.insert(lines, 'line' .. tostring(i))
  29. end
  30. table.insert(lines, '')
  31. feed_data(lines)
  32. screen:expect([[
  33. line26 |
  34. line27 |
  35. line28 |
  36. line29 |
  37. line30 |
  38. ^ |
  39. {3:-- TERMINAL --} |
  40. ]])
  41. end)
  42. it('will delete extra lines at the top', function()
  43. feed('<c-\\><c-n>gg')
  44. screen:expect([[
  45. ^line16 |
  46. line17 |
  47. line18 |
  48. line19 |
  49. line20 |
  50. line21 |
  51. |
  52. ]])
  53. end)
  54. end)
  55. describe('with cursor at last row', function()
  56. before_each(function()
  57. feed_data({ 'line1', 'line2', 'line3', 'line4', '' })
  58. screen:expect([[
  59. tty ready |
  60. line1 |
  61. line2 |
  62. line3 |
  63. line4 |
  64. ^ |
  65. {3:-- TERMINAL --} |
  66. ]])
  67. end)
  68. describe('and 1 line is printed', function()
  69. before_each(function()
  70. feed_data({ 'line5', '' })
  71. end)
  72. it('will hide the top line', function()
  73. screen:expect([[
  74. line1 |
  75. line2 |
  76. line3 |
  77. line4 |
  78. line5 |
  79. ^ |
  80. {3:-- TERMINAL --} |
  81. ]])
  82. eq(7, api.nvim_buf_line_count(0))
  83. end)
  84. describe('and then 3 more lines are printed', function()
  85. before_each(function()
  86. feed_data({ 'line6', 'line7', 'line8' })
  87. end)
  88. it('will hide the top 4 lines', function()
  89. screen:expect([[
  90. line3 |
  91. line4 |
  92. line5 |
  93. line6 |
  94. line7 |
  95. line8^ |
  96. {3:-- TERMINAL --} |
  97. ]])
  98. feed('<c-\\><c-n>6k')
  99. screen:expect([[
  100. ^line2 |
  101. line3 |
  102. line4 |
  103. line5 |
  104. line6 |
  105. line7 |
  106. |
  107. ]])
  108. feed('gg')
  109. screen:expect([[
  110. ^tty ready |
  111. line1 |
  112. line2 |
  113. line3 |
  114. line4 |
  115. line5 |
  116. |
  117. ]])
  118. feed('G')
  119. screen:expect([[
  120. line3 |
  121. line4 |
  122. line5 |
  123. line6 |
  124. line7 |
  125. ^line8 |
  126. |
  127. ]])
  128. end)
  129. end)
  130. end)
  131. describe('and height decreased by 1', function()
  132. local function will_hide_top_line()
  133. feed([[<C-\><C-N>]])
  134. screen:try_resize(screen._width - 2, screen._height - 1)
  135. screen:expect([[
  136. line2 |
  137. line3 |
  138. line4 |
  139. rows: 5, cols: 28 |
  140. ^ |
  141. |
  142. ]])
  143. end
  144. it('will hide top line', will_hide_top_line)
  145. describe('and then decreased by 2', function()
  146. before_each(function()
  147. will_hide_top_line()
  148. screen:try_resize(screen._width - 2, screen._height - 2)
  149. end)
  150. it('will hide the top 3 lines', function()
  151. screen:expect([[
  152. rows: 5, cols: 28 |
  153. rows: 3, cols: 26 |
  154. ^ |
  155. |
  156. ]])
  157. eq(8, api.nvim_buf_line_count(0))
  158. feed([[3k]])
  159. screen:expect([[
  160. ^line4 |
  161. rows: 5, cols: 28 |
  162. rows: 3, cols: 26 |
  163. |
  164. ]])
  165. end)
  166. end)
  167. end)
  168. end)
  169. describe('with empty lines after the cursor', function()
  170. -- XXX: Can't test this reliably on Windows unless the cursor is _moved_
  171. -- by the resize. http://docs.libuv.org/en/v1.x/signal.html
  172. -- See also: https://github.com/rprichard/winpty/issues/110
  173. if skip(is_os('win')) then
  174. return
  175. end
  176. describe('and the height is decreased by 2', function()
  177. before_each(function()
  178. screen:try_resize(screen._width, screen._height - 2)
  179. end)
  180. local function will_delete_last_two_lines()
  181. screen:expect([[
  182. tty ready |
  183. rows: 4, cols: 30 |
  184. ^ |
  185. |
  186. {3:-- TERMINAL --} |
  187. ]])
  188. eq(4, api.nvim_buf_line_count(0))
  189. end
  190. it('will delete the last two empty lines', will_delete_last_two_lines)
  191. describe('and then decreased by 1', function()
  192. before_each(function()
  193. will_delete_last_two_lines()
  194. screen:try_resize(screen._width, screen._height - 1)
  195. end)
  196. it('will delete the last line and hide the first', function()
  197. screen:expect([[
  198. rows: 4, cols: 30 |
  199. rows: 3, cols: 30 |
  200. ^ |
  201. {3:-- TERMINAL --} |
  202. ]])
  203. eq(4, api.nvim_buf_line_count(0))
  204. feed('<c-\\><c-n>gg')
  205. screen:expect([[
  206. ^tty ready |
  207. rows: 4, cols: 30 |
  208. rows: 3, cols: 30 |
  209. |
  210. ]])
  211. feed('a')
  212. screen:expect([[
  213. rows: 4, cols: 30 |
  214. rows: 3, cols: 30 |
  215. ^ |
  216. {3:-- TERMINAL --} |
  217. ]])
  218. end)
  219. end)
  220. end)
  221. end)
  222. describe('with 4 lines hidden in the scrollback', function()
  223. before_each(function()
  224. feed_data({ 'line1', 'line2', 'line3', 'line4', '' })
  225. screen:expect([[
  226. tty ready |
  227. line1 |
  228. line2 |
  229. line3 |
  230. line4 |
  231. ^ |
  232. {3:-- TERMINAL --} |
  233. ]])
  234. screen:try_resize(screen._width, screen._height - 3)
  235. screen:expect([[
  236. line4 |
  237. rows: 3, cols: 30 |
  238. ^ |
  239. {3:-- TERMINAL --} |
  240. ]])
  241. eq(7, api.nvim_buf_line_count(0))
  242. end)
  243. describe('and the height is increased by 1', function()
  244. -- XXX: Can't test this reliably on Windows unless the cursor is _moved_
  245. -- by the resize. http://docs.libuv.org/en/v1.x/signal.html
  246. -- See also: https://github.com/rprichard/winpty/issues/110
  247. if skip(is_os('win')) then
  248. return
  249. end
  250. local function pop_then_push()
  251. screen:try_resize(screen._width, screen._height + 1)
  252. screen:expect([[
  253. line4 |
  254. rows: 3, cols: 30 |
  255. rows: 4, cols: 30 |
  256. ^ |
  257. {3:-- TERMINAL --} |
  258. ]])
  259. end
  260. it('will pop 1 line and then push it back', pop_then_push)
  261. describe('and then by 3', function()
  262. before_each(function()
  263. pop_then_push()
  264. eq(8, api.nvim_buf_line_count(0))
  265. screen:try_resize(screen._width, screen._height + 3)
  266. end)
  267. local function pop3_then_push1()
  268. screen:expect([[
  269. line2 |
  270. line3 |
  271. line4 |
  272. rows: 3, cols: 30 |
  273. rows: 4, cols: 30 |
  274. rows: 7, cols: 30 |
  275. ^ |
  276. {3:-- TERMINAL --} |
  277. ]])
  278. eq(9, api.nvim_buf_line_count(0))
  279. feed('<c-\\><c-n>gg')
  280. screen:expect([[
  281. ^tty ready |
  282. line1 |
  283. line2 |
  284. line3 |
  285. line4 |
  286. rows: 3, cols: 30 |
  287. rows: 4, cols: 30 |
  288. |
  289. ]])
  290. end
  291. it('will pop 3 lines and then push one back', pop3_then_push1)
  292. describe('and then by 4', function()
  293. before_each(function()
  294. pop3_then_push1()
  295. feed('Gi')
  296. screen:try_resize(screen._width, screen._height + 4)
  297. end)
  298. it('will show all lines and leave a blank one at the end', function()
  299. screen:expect([[
  300. tty ready |
  301. line1 |
  302. line2 |
  303. line3 |
  304. line4 |
  305. rows: 3, cols: 30 |
  306. rows: 4, cols: 30 |
  307. rows: 7, cols: 30 |
  308. rows: 11, cols: 30 |
  309. ^ |
  310. |
  311. {3:-- TERMINAL --} |
  312. ]])
  313. -- since there's an empty line after the cursor, the buffer line
  314. -- count equals the terminal screen height
  315. eq(11, api.nvim_buf_line_count(0))
  316. end)
  317. end)
  318. end)
  319. end)
  320. end)
  321. end)
  322. describe(':terminal prints more lines than the screen height and exits', function()
  323. it('will push extra lines to scrollback', function()
  324. clear()
  325. local screen = Screen.new(30, 7, { rgb = false })
  326. command(
  327. ("call jobstart(['%s', '10'], {'term':v:true}) | startinsert"):format(testprg('tty-test'))
  328. )
  329. screen:expect([[
  330. line6 |
  331. line7 |
  332. line8 |
  333. line9 |
  334. |
  335. [Process exited 0]^ |
  336. {5:-- TERMINAL --} |
  337. ]])
  338. feed('<cr>')
  339. -- closes the buffer correctly after pressing a key
  340. screen:expect {
  341. grid = [[
  342. ^ |
  343. {1:~ }|*5
  344. |
  345. ]],
  346. attr_ids = { [1] = { foreground = 12 } },
  347. }
  348. end)
  349. end)
  350. describe("'scrollback' option", function()
  351. before_each(function()
  352. clear()
  353. end)
  354. local function set_fake_shell()
  355. api.nvim_set_option_value('shell', string.format('"%s" INTERACT', testprg('shell-test')), {})
  356. end
  357. local function expect_lines(expected, epsilon)
  358. local ep = epsilon and epsilon or 0
  359. local actual = eval("line('$')")
  360. if expected > actual + ep and expected < actual - ep then
  361. error('expected (+/- ' .. ep .. '): ' .. expected .. ', actual: ' .. tostring(actual))
  362. end
  363. end
  364. it('set to 0 behaves as 1', function()
  365. local screen
  366. if is_os('win') then
  367. screen = tt.setup_screen(nil, { 'cmd.exe' }, 30)
  368. else
  369. screen = tt.setup_screen(nil, { 'sh' }, 30)
  370. end
  371. api.nvim_set_option_value('scrollback', 0, {})
  372. feed_data(('%s REP 31 line%s'):format(testprg('shell-test'), is_os('win') and '\r' or '\n'))
  373. screen:expect { any = '30: line ' }
  374. retry(nil, nil, function()
  375. expect_lines(7)
  376. end)
  377. end)
  378. it('deletes lines (only) if necessary', function()
  379. local screen
  380. if is_os('win') then
  381. command([[let $PROMPT='$$']])
  382. screen = tt.setup_screen(nil, { 'cmd.exe' }, 30)
  383. else
  384. command('let $PS1 = "$"')
  385. screen = tt.setup_screen(nil, { 'sh' }, 30)
  386. end
  387. api.nvim_set_option_value('scrollback', 200, {})
  388. -- Wait for prompt.
  389. screen:expect { any = '%$' }
  390. feed_data(('%s REP 31 line%s'):format(testprg('shell-test'), is_os('win') and '\r' or '\n'))
  391. screen:expect { any = '30: line ' }
  392. retry(nil, nil, function()
  393. expect_lines(33, 2)
  394. end)
  395. api.nvim_set_option_value('scrollback', 10, {})
  396. poke_eventloop()
  397. retry(nil, nil, function()
  398. expect_lines(16)
  399. end)
  400. api.nvim_set_option_value('scrollback', 10000, {})
  401. retry(nil, nil, function()
  402. expect_lines(16)
  403. end)
  404. -- Terminal job data is received asynchronously, may happen before the
  405. -- 'scrollback' option is synchronized with the internal sb_buffer.
  406. command('sleep 100m')
  407. feed_data(('%s REP 41 line%s'):format(testprg('shell-test'), is_os('win') and '\r' or '\n'))
  408. if is_os('win') then
  409. screen:expect {
  410. grid = [[
  411. 37: line |
  412. 38: line |
  413. 39: line |
  414. 40: line |
  415. |
  416. $^ |
  417. {3:-- TERMINAL --} |
  418. ]],
  419. }
  420. else
  421. screen:expect {
  422. grid = [[
  423. 36: line |
  424. 37: line |
  425. 38: line |
  426. 39: line |
  427. 40: line |
  428. {MATCH:.*}|
  429. {3:-- TERMINAL --} |
  430. ]],
  431. }
  432. end
  433. expect_lines(58)
  434. -- Verify off-screen state
  435. eq((is_os('win') and '36: line' or '35: line'), eval("getline(line('w0') - 1)->trim(' ', 2)"))
  436. eq((is_os('win') and '27: line' or '26: line'), eval("getline(line('w0') - 10)->trim(' ', 2)"))
  437. end)
  438. it('deletes extra lines immediately', function()
  439. -- Scrollback is 10 on setup_screen
  440. local screen = tt.setup_screen(nil, nil, 30)
  441. local lines = {}
  442. for i = 1, 30 do
  443. table.insert(lines, 'line' .. tostring(i))
  444. end
  445. table.insert(lines, '')
  446. feed_data(lines)
  447. screen:expect([[
  448. line26 |
  449. line27 |
  450. line28 |
  451. line29 |
  452. line30 |
  453. ^ |
  454. {3:-- TERMINAL --} |
  455. ]])
  456. local term_height = 6 -- Actual terminal screen height, not the scrollback
  457. -- Initial
  458. local scrollback = api.nvim_get_option_value('scrollback', {})
  459. eq(scrollback + term_height, eval('line("$")'))
  460. -- Reduction
  461. scrollback = scrollback - 2
  462. api.nvim_set_option_value('scrollback', scrollback, {})
  463. eq(scrollback + term_height, eval('line("$")'))
  464. end)
  465. it('defaults to 10000 in :terminal buffers', function()
  466. set_fake_shell()
  467. command('terminal')
  468. eq(10000, api.nvim_get_option_value('scrollback', {}))
  469. end)
  470. it('error if set to invalid value', function()
  471. eq('Vim(set):E474: Invalid argument: scrollback=-2', pcall_err(command, 'set scrollback=-2'))
  472. eq(
  473. 'Vim(set):E474: Invalid argument: scrollback=100001',
  474. pcall_err(command, 'set scrollback=100001')
  475. )
  476. end)
  477. it('defaults to -1 on normal buffers', function()
  478. command('new')
  479. eq(-1, api.nvim_get_option_value('scrollback', {}))
  480. end)
  481. it(':setlocal in a :terminal buffer', function()
  482. set_fake_shell()
  483. -- _Global_ scrollback=-1 defaults :terminal to 10_000.
  484. command('setglobal scrollback=-1')
  485. command('terminal')
  486. eq(10000, api.nvim_get_option_value('scrollback', {}))
  487. -- _Local_ scrollback=-1 in :terminal forces the _maximum_.
  488. command('setlocal scrollback=-1')
  489. retry(nil, nil, function() -- Fixup happens on refresh, not immediately.
  490. eq(100000, api.nvim_get_option_value('scrollback', {}))
  491. end)
  492. -- _Local_ scrollback=-1 during TermOpen forces the maximum. #9605
  493. command('setglobal scrollback=-1')
  494. command('autocmd TermOpen * setlocal scrollback=-1')
  495. command('terminal')
  496. eq(100000, api.nvim_get_option_value('scrollback', {}))
  497. end)
  498. it(':setlocal in a normal buffer', function()
  499. command('new')
  500. -- :setlocal to -1.
  501. command('setlocal scrollback=-1')
  502. eq(-1, api.nvim_get_option_value('scrollback', {}))
  503. -- :setlocal to anything except -1. Currently, this just has no effect.
  504. command('setlocal scrollback=42')
  505. eq(42, api.nvim_get_option_value('scrollback', {}))
  506. end)
  507. it(':set updates local value and global default', function()
  508. set_fake_shell()
  509. command('set scrollback=42') -- set global value
  510. eq(42, api.nvim_get_option_value('scrollback', {}))
  511. command('terminal')
  512. eq(42, api.nvim_get_option_value('scrollback', {})) -- inherits global default
  513. command('setlocal scrollback=99')
  514. eq(99, api.nvim_get_option_value('scrollback', {}))
  515. command('set scrollback<') -- reset to global default
  516. eq(42, api.nvim_get_option_value('scrollback', {}))
  517. command('setglobal scrollback=734') -- new global default
  518. eq(42, api.nvim_get_option_value('scrollback', {})) -- local value did not change
  519. command('terminal')
  520. eq(734, api.nvim_get_option_value('scrollback', {}))
  521. end)
  522. end)
  523. describe('pending scrollback line handling', function()
  524. local screen
  525. before_each(function()
  526. clear()
  527. screen = Screen.new(30, 7)
  528. screen:set_default_attr_ids {
  529. [1] = { foreground = Screen.colors.Brown },
  530. [2] = { reverse = true },
  531. [3] = { bold = true },
  532. }
  533. end)
  534. it("does not crash after setting 'number' #14891", function()
  535. exec_lua [[
  536. local api = vim.api
  537. local buf = api.nvim_create_buf(true, true)
  538. local chan = api.nvim_open_term(buf, {})
  539. vim.wo.number = true
  540. api.nvim_chan_send(chan, ("a\n"):rep(11) .. "a")
  541. api.nvim_win_set_buf(0, buf)
  542. ]]
  543. screen:expect [[
  544. {1: 1 }^a |
  545. {1: 2 }a |
  546. {1: 3 }a |
  547. {1: 4 }a |
  548. {1: 5 }a |
  549. {1: 6 }a |
  550. |
  551. ]]
  552. feed('G')
  553. screen:expect [[
  554. {1: 7 }a |
  555. {1: 8 }a |
  556. {1: 9 }a |
  557. {1: 10 }a |
  558. {1: 11 }a |
  559. {1: 12 }^a |
  560. |
  561. ]]
  562. assert_alive()
  563. end)
  564. it('does not crash after nvim_buf_call #14891', function()
  565. exec_lua(
  566. [[
  567. local bufnr = vim.api.nvim_create_buf(false, true)
  568. local args = ...
  569. vim.api.nvim_buf_call(bufnr, function()
  570. vim.fn.jobstart(args, { term = true })
  571. end)
  572. vim.api.nvim_win_set_buf(0, bufnr)
  573. vim.cmd('startinsert')
  574. ]],
  575. is_os('win') and { 'cmd.exe', '/c', 'for /L %I in (1,1,12) do @echo hi' }
  576. or { 'printf', ('hi\n'):rep(12) }
  577. )
  578. screen:expect [[
  579. hi |*4
  580. |
  581. [Process exited 0]^ |
  582. {3:-- TERMINAL --} |
  583. ]]
  584. assert_alive()
  585. end)
  586. end)