ui_event_spec.lua 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. local t = require('test.testutil')
  2. local n = require('test.functional.testnvim')()
  3. local Screen = require('test.functional.ui.screen')
  4. local eq = t.eq
  5. local exec_lua = n.exec_lua
  6. local clear = n.clear
  7. local feed = n.feed
  8. local fn = n.fn
  9. local assert_log = t.assert_log
  10. local check_close = n.check_close
  11. local testlog = 'Xtest_lua_ui_event_log'
  12. describe('vim.ui_attach', function()
  13. local screen
  14. before_each(function()
  15. clear()
  16. exec_lua [[
  17. ns = vim.api.nvim_create_namespace 'testspace'
  18. events = {}
  19. function on_event(event, ...)
  20. events[#events+1] = {event, ...}
  21. return true
  22. end
  23. function get_events()
  24. local ret_events = events
  25. events = {}
  26. return ret_events
  27. end
  28. ]]
  29. screen = Screen.new(40, 5)
  30. end)
  31. local function expect_events(expected)
  32. local evs = exec_lua 'return get_events(...)'
  33. eq(expected, evs, vim.inspect(evs))
  34. end
  35. it('can receive popupmenu events', function()
  36. exec_lua [[ vim.ui_attach(ns, {ext_popupmenu=true}, on_event) ]]
  37. feed('ifo')
  38. screen:expect {
  39. grid = [[
  40. fo^ |
  41. {1:~ }|*3
  42. {5:-- INSERT --} |
  43. ]],
  44. }
  45. fn.complete(1, { 'food', 'foobar', 'foo' })
  46. screen:expect {
  47. grid = [[
  48. food^ |
  49. {1:~ }|*3
  50. {5:-- INSERT --} |
  51. ]],
  52. }
  53. expect_events {
  54. {
  55. 'popupmenu_show',
  56. { { 'food', '', '', '' }, { 'foobar', '', '', '' }, { 'foo', '', '', '' } },
  57. 0,
  58. 0,
  59. 0,
  60. 1,
  61. },
  62. }
  63. feed '<c-n>'
  64. screen:expect {
  65. grid = [[
  66. foobar^ |
  67. {1:~ }|*3
  68. {5:-- INSERT --} |
  69. ]],
  70. }
  71. expect_events {
  72. { 'popupmenu_select', 1 },
  73. }
  74. feed '<c-y>'
  75. screen:expect_unchanged()
  76. expect_events {
  77. { 'popupmenu_hide' },
  78. }
  79. -- vim.ui_detach() stops events, and reenables builtin pum immediately
  80. exec_lua [[
  81. vim.ui_detach(ns)
  82. vim.fn.complete(1, {'food', 'foobar', 'foo'})
  83. ]]
  84. screen:expect {
  85. grid = [[
  86. food^ |
  87. {12:food }{1: }|
  88. {4:foobar }{1: }|
  89. {4:foo }{1: }|
  90. {5:-- INSERT --} |
  91. ]],
  92. }
  93. expect_events {}
  94. end)
  95. it('does not crash on exit', function()
  96. local p = n.spawn_wait(
  97. '--cmd',
  98. [[ lua ns = vim.api.nvim_create_namespace 'testspace' ]],
  99. '--cmd',
  100. [[ lua vim.ui_attach(ns, {ext_popupmenu=true}, function() end) ]],
  101. '--cmd',
  102. 'quitall!'
  103. )
  104. eq(0, p.status)
  105. end)
  106. it('can receive accurate message kinds even if they are history', function()
  107. exec_lua([[
  108. vim.cmd.echomsg("'message1'")
  109. print('message2')
  110. vim.ui_attach(ns, { ext_messages = true }, on_event)
  111. vim.cmd.echomsg("'message3'")
  112. ]])
  113. feed(':messages<cr>')
  114. feed('<cr>')
  115. local actual = exec_lua([[
  116. return vim.tbl_filter(function (event)
  117. return event[1] == "msg_history_show"
  118. end, events)
  119. ]])
  120. eq({
  121. {
  122. 'msg_history_show',
  123. {
  124. { 'echomsg', { { 0, 'message1', 0 } } },
  125. { 'lua_print', { { 0, 'message2', 0 } } },
  126. { 'echomsg', { { 0, 'message3', 0 } } },
  127. },
  128. },
  129. }, actual, vim.inspect(actual))
  130. end)
  131. it('ui_refresh() activates correct capabilities without remote UI', function()
  132. screen:detach()
  133. exec_lua('vim.ui_attach(ns, { ext_cmdline = true }, on_event)')
  134. eq(1, n.api.nvim_get_option_value('cmdheight', {}))
  135. exec_lua('vim.ui_detach(ns)')
  136. exec_lua('vim.ui_attach(ns, { ext_messages = true }, on_event)')
  137. n.api.nvim_set_option_value('cmdheight', 1, {})
  138. screen:attach()
  139. eq(1, n.api.nvim_get_option_value('cmdheight', {}))
  140. end)
  141. it("ui_refresh() sets 'cmdheight' for all open tabpages with ext_messages", function()
  142. exec_lua('vim.cmd.tabnew()')
  143. exec_lua('vim.ui_attach(ns, { ext_messages = true }, on_event)')
  144. exec_lua('vim.cmd.tabnext()')
  145. eq(0, n.api.nvim_get_option_value('cmdheight', {}))
  146. end)
  147. it("can attach ext_messages without changing 'cmdheight'", function()
  148. exec_lua('vim.ui_attach(ns, { ext_messages = true, set_cmdheight = false }, on_event)')
  149. eq(1, n.api.nvim_get_option_value('cmdheight', {}))
  150. end)
  151. it('avoids recursive flushing and invalid memory access with :redraw', function()
  152. exec_lua([[
  153. _G.cmdline = 0
  154. vim.ui_attach(ns, { ext_messages = true }, function(ev)
  155. if ev == 'msg_show' then
  156. vim.schedule(function() vim.cmd.redraw() end)
  157. elseif ev:find('cmdline') then
  158. _G.cmdline = _G.cmdline + (ev == 'cmdline_show' and 1 or 0)
  159. vim.api.nvim_buf_set_lines(0, 0, -1, false, { tostring(_G.cmdline) })
  160. vim.cmd('redraw')
  161. end
  162. end
  163. )]])
  164. screen:expect([[
  165. ^ |
  166. {1:~ }|*4
  167. ]])
  168. feed(':')
  169. screen:expect({
  170. grid = [[
  171. ^1 |
  172. {1:~ }|*4
  173. ]],
  174. cmdline = { {
  175. content = { { '' } },
  176. firstc = ':',
  177. pos = 0,
  178. } },
  179. })
  180. feed('version<CR><CR>v<Esc>')
  181. screen:expect({
  182. grid = [[
  183. ^2 |
  184. {1:~ }|*4
  185. ]],
  186. cmdline = { { abort = false } },
  187. })
  188. feed([[:call confirm("Save changes?", "&Yes\n&No\n&Cancel")<CR>]])
  189. screen:expect({
  190. grid = [[
  191. ^4 |
  192. {1:~ }|*4
  193. ]],
  194. cmdline = {
  195. {
  196. content = { { '' } },
  197. hl_id = 10,
  198. pos = 0,
  199. prompt = '[Y]es, (N)o, (C)ancel: ',
  200. },
  201. },
  202. messages = {
  203. {
  204. content = { { '\nSave changes?\n', 6, 10 } },
  205. history = false,
  206. kind = 'confirm',
  207. },
  208. },
  209. })
  210. feed('n')
  211. screen:expect({
  212. grid = [[
  213. ^4 |
  214. {1:~ }|*4
  215. ]],
  216. cmdline = { { abort = false } },
  217. })
  218. end)
  219. it("preserved 'incsearch/command' screen state after :redraw from ext_cmdline", function()
  220. exec_lua([[
  221. vim.cmd.norm('ifoobar')
  222. vim.cmd('1split cmdline')
  223. local buf = vim.api.nvim_get_current_buf()
  224. vim.cmd.wincmd('p')
  225. vim.ui_attach(ns, { ext_cmdline = true }, function(event, ...)
  226. if event == 'cmdline_show' then
  227. local content = select(1, ...)
  228. vim.api.nvim_buf_set_lines(buf, -2, -1, false, {content[1][2]})
  229. vim.cmd('redraw')
  230. end
  231. return true
  232. end)
  233. ]])
  234. -- Updates a cmdline window
  235. feed(':cmdline')
  236. screen:expect({
  237. grid = [[
  238. cmdline |
  239. {2:cmdline [+] }|
  240. fooba^r |
  241. {3:[No Name] [+] }|
  242. |
  243. ]],
  244. })
  245. -- Does not clear 'incsearch' highlighting
  246. feed('<Esc>/foo')
  247. screen:expect({
  248. grid = [[
  249. foo |
  250. {2:cmdline [+] }|
  251. {2:foo}ba^r |
  252. {3:[No Name] [+] }|
  253. |
  254. ]],
  255. })
  256. -- Shows new cmdline state during 'inccommand'
  257. feed('<Esc>:%s/bar/baz')
  258. screen:expect({
  259. grid = [[
  260. %s/bar/baz |
  261. {2:cmdline [+] }|
  262. foo{10:ba^z} |
  263. {3:[No Name] [+] }|
  264. |
  265. ]],
  266. })
  267. end)
  268. it('msg_show in fast context', function()
  269. exec_lua([[
  270. vim.ui_attach(ns, { ext_messages = true }, function(event, _, content)
  271. if event == "msg_show" then
  272. vim.api.nvim_get_runtime_file("foo", false)
  273. -- non-"fast-api" is not allowed in msg_show callback and should be scheduled
  274. local _, err = pcall(vim.api.nvim_buf_set_lines, 0, -2, -1, false, { content[1][2] })
  275. pcall(vim.api.nvim__redraw, { flush = true })
  276. vim.schedule(function()
  277. vim.api.nvim_buf_set_lines(0, -2, -1, false, { content[1][2], err })
  278. end)
  279. end
  280. end)
  281. ]])
  282. -- "fast-api" does not prevent aborting :function
  283. feed(':func Foo()<cr>bar<cr>endf<cr>:func Foo()<cr>')
  284. screen:expect({
  285. grid = [[
  286. ^E122: Function Foo already exists, add !|
  287. to replace it |
  288. E5560: nvim_buf_set_lines must not be ca|
  289. lled in a fast event context |
  290. {1:~ }|
  291. ]],
  292. cmdline = { { abort = false } },
  293. messages = {
  294. {
  295. content = { { 'E122: Function Foo already exists, add ! to replace it', 9, 6 } },
  296. history = true,
  297. kind = 'emsg',
  298. },
  299. },
  300. })
  301. end)
  302. it('ext_cmdline completion popupmenu', function()
  303. screen:try_resize(screen._width, 10)
  304. screen:add_extra_attr_ids { [100] = { background = Screen.colors.Black } }
  305. exec_lua([[
  306. vim.o.wildoptions = 'pum'
  307. local buf = vim.api.nvim_create_buf(false, true)
  308. vim.cmd('call setline(1, range(1, 10))')
  309. _G.win = vim.api.nvim_open_win(buf, false, {
  310. relative = 'editor',
  311. col = 3,
  312. row = 3,
  313. width = 20,
  314. height = 1,
  315. style = 'minimal',
  316. focusable = false,
  317. zindex = 300,
  318. _cmdline_offset = 0,
  319. })
  320. vim.ui_attach(ns, { ext_cmdline = true }, function(event, content, _, firstc)
  321. if event == 'cmdline_show' then
  322. local prompt = vim.api.nvim_win_get_config(_G.win)._cmdline_offset == 0
  323. prompt = (prompt and firstc or 'Excommand:') .. content[1][2]
  324. vim.api.nvim_buf_set_lines(buf, -2, -1, false, { prompt })
  325. vim.api.nvim_win_set_cursor(_G.win, { 1, #prompt })
  326. vim.api.nvim__redraw({ win = _G.win, cursor = true, flush = true })
  327. end
  328. return true
  329. end)
  330. vim.api.nvim_set_hl(0, 'Pmenu', {})
  331. ]])
  332. feed(':call buf<tab>')
  333. screen:expect([[
  334. 1 |
  335. 2 |
  336. 3 |
  337. 4 :call bufadd^( |
  338. 5 {12: bufadd( }{100: } |
  339. 6 bufexists( {100: } |
  340. 7 buffer_exists( {12: } |
  341. 8 buffer_name( {12: } |
  342. 9 buffer_number( {12: } |
  343. |
  344. ]])
  345. exec_lua([[
  346. vim.api.nvim_win_set_config(_G.win, {
  347. relative = 'editor',
  348. col = 0,
  349. row = 1000,
  350. width = 1000,
  351. height = 1,
  352. })
  353. vim.api.nvim__redraw({flush = true})
  354. ]])
  355. screen:expect([[
  356. 1 |
  357. 2 |
  358. 3 |
  359. 4 |
  360. 5 {12: bufadd( }{100: } |
  361. 6 bufexists( {100: } |
  362. 7 buffer_exists( {12: } |
  363. 8 buffer_name( {12: } |
  364. 9 buffer_number( {12: } |
  365. :call bufadd^( |
  366. ]])
  367. feed('<tab>')
  368. screen:expect([[
  369. 1 bufadd( {100: } |
  370. 2 {12: bufexists( }{100: } |
  371. 3 buffer_exists( {100: } |
  372. 4 buffer_name( {100: } |
  373. 5 buffer_number( {100: } |
  374. 6 buflisted( {100: } |
  375. 7 bufload( {12: } |
  376. 8 bufloaded( {12: } |
  377. 9 bufname( {12: } |
  378. :call bufexists^( |
  379. ]])
  380. -- Test different offset (e.g. for custom prompt)
  381. exec_lua('vim.api.nvim_win_set_config(_G.win, { _cmdline_offset = 9 })')
  382. feed('<Esc>:call buf<Tab>')
  383. screen:expect([[
  384. 1 {12: bufadd( }{100: } |
  385. 2 bufexists( {100: } |
  386. 3 buffer_exists( {100: } |
  387. 4 buffer_name( {100: } |
  388. 5 buffer_number( {100: } |
  389. 6 buflisted( {100: } |
  390. 7 bufload( {12: } |
  391. 8 bufloaded( {12: } |
  392. 9 bufname( {12: } |
  393. Excommand:call bufadd^( |
  394. ]])
  395. end)
  396. end)
  397. describe('vim.ui_attach', function()
  398. before_each(function()
  399. clear({ env = { NVIM_LOG_FILE = testlog } })
  400. end)
  401. after_each(function()
  402. check_close()
  403. os.remove(testlog)
  404. end)
  405. it('callback error is logged', function()
  406. exec_lua([[
  407. local ns = vim.api.nvim_create_namespace('test')
  408. vim.ui_attach(ns, { ext_popupmenu = true }, function() error(42) end)
  409. ]])
  410. feed('ifoo<CR>foobar<CR>fo<C-X><C-N>')
  411. assert_log(
  412. 'Error in "popupmenu_show" UI event handler %(ns=test%):[\r\n\t ]+Lua: .*: 42',
  413. testlog,
  414. 100
  415. )
  416. end)
  417. it('detaches after excessive errors', function()
  418. local screen = Screen.new(86, 10)
  419. screen:add_extra_attr_ids({ [100] = { bold = true, foreground = Screen.colors.SeaGreen } })
  420. exec_lua([[
  421. vim.ui_attach(vim.api.nvim_create_namespace(''), { ext_messages = true }, function(ev)
  422. if ev:find('msg') then
  423. vim.api.nvim_buf_set_lines(0, -2, -1, false, { err[1] })
  424. end
  425. end)
  426. ]])
  427. local s1 = [[
  428. ^ |
  429. {1:~ }|*9
  430. ]]
  431. screen:expect(s1)
  432. feed('Q<CR>')
  433. screen:expect({
  434. grid = s1,
  435. messages = {
  436. {
  437. content = { { "E354: Invalid register name: '^@'", 9, 6 } },
  438. history = true,
  439. kind = 'emsg',
  440. },
  441. {
  442. content = {
  443. {
  444. 'Lua callback:\n[string "<nvim>"]:3: attempt to index global \'err\' (a nil value)\nstack traceback:\n\t[string "<nvim>"]:3: in function <[string "<nvim>"]:1>',
  445. 9,
  446. 6,
  447. },
  448. },
  449. history = true,
  450. kind = 'lua_error',
  451. },
  452. {
  453. content = { { 'Press ENTER or type command to continue', 100, 18 } },
  454. history = false,
  455. kind = 'return_prompt',
  456. },
  457. },
  458. })
  459. feed('<CR>:messages<CR>')
  460. screen:expect([[
  461. {9:Error in "msg_show" UI event handler (ns=(UNKNOWN PLUGIN)):} |
  462. {9:Lua: [string "<nvim>"]:3: attempt to index global 'err' (a nil value)} |
  463. {9:stack traceback:} |
  464. {9: [string "<nvim>"]:3: in function <[string "<nvim>"]:1>} |
  465. {9:Error in "msg_clear" UI event handler (ns=(UNKNOWN PLUGIN)):} |
  466. {9:Lua: [string "<nvim>"]:3: attempt to index global 'err' (a nil value)} |
  467. {9:stack traceback:} |
  468. {9: [string "<nvim>"]:3: in function <[string "<nvim>"]:1>} |
  469. {9:Excessive errors in vim.ui_attach() callback (ns=(UNKNOWN PLUGIN))} |
  470. {100:Press ENTER or type command to continue}^ |
  471. ]])
  472. feed('<CR>')
  473. -- Also when scheduled
  474. exec_lua([[
  475. vim.ui_attach(vim.api.nvim_create_namespace(''), { ext_messages = true }, function()
  476. vim.schedule(function() vim.api.nvim_buf_set_lines(0, -2, -1, false, { err[1] }) end)
  477. end)
  478. ]])
  479. screen:expect({
  480. grid = s1,
  481. messages = {
  482. {
  483. content = {
  484. {
  485. 'vim.schedule callback: [string "<nvim>"]:2: attempt to index global \'err\' (a nil value)\nstack traceback:\n\t[string "<nvim>"]:2: in function <[string "<nvim>"]:2>',
  486. 9,
  487. 6,
  488. },
  489. },
  490. history = true,
  491. kind = 'lua_error',
  492. },
  493. {
  494. content = {
  495. {
  496. 'vim.schedule callback: [string "<nvim>"]:2: attempt to index global \'err\' (a nil value)\nstack traceback:\n\t[string "<nvim>"]:2: in function <[string "<nvim>"]:2>',
  497. 9,
  498. 6,
  499. },
  500. },
  501. history = true,
  502. kind = 'lua_error',
  503. },
  504. {
  505. content = { { 'Press ENTER or type command to continue', 100, 18 } },
  506. history = false,
  507. kind = 'return_prompt',
  508. },
  509. },
  510. })
  511. feed('<Esc>:1messages clear<cr>:messages<CR>')
  512. screen:expect([[
  513. ^ |
  514. {1:~ }|*8
  515. {9:Excessive errors in vim.ui_attach() callback (ns=(UNKNOWN PLUGIN))} |
  516. ]])
  517. end)
  518. it('sourcing invalid file does not crash #32166', function()
  519. exec_lua([[
  520. local ns = vim.api.nvim_create_namespace("")
  521. vim.ui_attach(ns, { ext_messages = true }, function() end)
  522. ]])
  523. feed((':luafile %s<CR>'):format(testlog))
  524. n.assert_alive()
  525. end)
  526. end)