ui_event_spec.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  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('avoids recursive flushing and invalid memory access with :redraw', function()
  148. exec_lua([[
  149. _G.cmdline = 0
  150. vim.ui_attach(ns, { ext_messages = true }, function(ev)
  151. if ev == 'msg_show' then
  152. vim.schedule(function() vim.cmd.redraw() end)
  153. elseif ev:find('cmdline') then
  154. _G.cmdline = _G.cmdline + (ev == 'cmdline_show' and 1 or 0)
  155. vim.api.nvim_buf_set_lines(0, 0, -1, false, { tostring(_G.cmdline) })
  156. vim.cmd('redraw')
  157. end
  158. end
  159. )]])
  160. screen:expect([[
  161. ^ |
  162. {1:~ }|*4
  163. ]])
  164. feed(':')
  165. screen:expect({
  166. grid = [[
  167. ^1 |
  168. {1:~ }|*4
  169. ]],
  170. cmdline = { {
  171. content = { { '' } },
  172. firstc = ':',
  173. pos = 0,
  174. } },
  175. })
  176. feed('version<CR><CR>v<Esc>')
  177. screen:expect({
  178. grid = [[
  179. ^2 |
  180. {1:~ }|*4
  181. ]],
  182. cmdline = { { abort = false } },
  183. })
  184. feed([[:call confirm("Save changes?", "&Yes\n&No\n&Cancel")<CR>]])
  185. screen:expect({
  186. grid = [[
  187. ^4 |
  188. {1:~ }|*4
  189. ]],
  190. cmdline = {
  191. {
  192. content = { { '' } },
  193. hl_id = 10,
  194. pos = 0,
  195. prompt = '[Y]es, (N)o, (C)ancel: ',
  196. },
  197. },
  198. messages = {
  199. {
  200. content = { { '\nSave changes?\n', 6, 10 } },
  201. history = false,
  202. kind = 'confirm',
  203. },
  204. },
  205. })
  206. feed('n')
  207. screen:expect({
  208. grid = [[
  209. ^4 |
  210. {1:~ }|*4
  211. ]],
  212. cmdline = { { abort = false } },
  213. })
  214. end)
  215. it("preserved 'incsearch/command' screen state after :redraw from ext_cmdline", function()
  216. exec_lua([[
  217. vim.cmd.norm('ifoobar')
  218. vim.cmd('1split cmdline')
  219. local buf = vim.api.nvim_get_current_buf()
  220. vim.cmd.wincmd('p')
  221. vim.ui_attach(ns, { ext_cmdline = true }, function(event, ...)
  222. if event == 'cmdline_show' then
  223. local content = select(1, ...)
  224. vim.api.nvim_buf_set_lines(buf, -2, -1, false, {content[1][2]})
  225. vim.cmd('redraw')
  226. end
  227. return true
  228. end)
  229. ]])
  230. -- Updates a cmdline window
  231. feed(':cmdline')
  232. screen:expect({
  233. grid = [[
  234. cmdline |
  235. {2:cmdline [+] }|
  236. fooba^r |
  237. {3:[No Name] [+] }|
  238. |
  239. ]],
  240. })
  241. -- Does not clear 'incsearch' highlighting
  242. feed('<Esc>/foo')
  243. screen:expect({
  244. grid = [[
  245. foo |
  246. {2:cmdline [+] }|
  247. {2:foo}ba^r |
  248. {3:[No Name] [+] }|
  249. |
  250. ]],
  251. })
  252. -- Shows new cmdline state during 'inccommand'
  253. feed('<Esc>:%s/bar/baz')
  254. screen:expect({
  255. grid = [[
  256. %s/bar/baz |
  257. {2:cmdline [+] }|
  258. foo{10:ba^z} |
  259. {3:[No Name] [+] }|
  260. |
  261. ]],
  262. })
  263. end)
  264. it('msg_show in fast context', function()
  265. exec_lua([[
  266. vim.ui_attach(ns, { ext_messages = true }, function(event, _, content)
  267. if event == "msg_show" then
  268. vim.api.nvim_get_runtime_file("foo", false)
  269. -- non-"fast-api" is not allowed in msg_show callback and should be scheduled
  270. local _, err = pcall(vim.api.nvim_buf_set_lines, 0, -2, -1, false, { content[1][2] })
  271. pcall(vim.api.nvim__redraw, { flush = true })
  272. vim.schedule(function()
  273. vim.api.nvim_buf_set_lines(0, -2, -1, false, { content[1][2], err })
  274. end)
  275. end
  276. end)
  277. ]])
  278. -- "fast-api" does not prevent aborting :function
  279. feed(':func Foo()<cr>bar<cr>endf<cr>:func Foo()<cr>')
  280. screen:expect({
  281. grid = [[
  282. ^E122: Function Foo already exists, add !|
  283. to replace it |
  284. E5560: nvim_buf_set_lines must not be ca|
  285. lled in a fast event context |
  286. {1:~ }|
  287. ]],
  288. cmdline = { { abort = false } },
  289. messages = {
  290. {
  291. content = { { 'E122: Function Foo already exists, add ! to replace it', 9, 6 } },
  292. history = true,
  293. kind = 'emsg',
  294. },
  295. },
  296. })
  297. end)
  298. end)
  299. describe('vim.ui_attach', function()
  300. local screen
  301. before_each(function()
  302. clear({ env = { NVIM_LOG_FILE = testlog } })
  303. screen = Screen.new(40, 5)
  304. end)
  305. after_each(function()
  306. check_close()
  307. os.remove(testlog)
  308. end)
  309. it('error in callback is logged', function()
  310. exec_lua([[
  311. local ns = vim.api.nvim_create_namespace('testspace')
  312. vim.ui_attach(ns, { ext_popupmenu = true }, function() error(42) end)
  313. ]])
  314. feed('ifoo<CR>foobar<CR>fo<C-X><C-N>')
  315. assert_log('Error executing UI event callback: Error executing lua: .*: 42', testlog, 100)
  316. end)
  317. it('detaches after excessive errors', function()
  318. screen:add_extra_attr_ids({ [100] = { bold = true, foreground = Screen.colors.SeaGreen } })
  319. exec_lua([[
  320. vim.ui_attach(vim.api.nvim_create_namespace(''), { ext_messages = true }, function()
  321. vim.api.nvim_buf_set_lines(0, -2, -1, false, { err[1] })
  322. end)
  323. ]])
  324. local s1 = [[
  325. ^ |
  326. {1:~ }|*4
  327. ]]
  328. screen:expect(s1)
  329. feed('QQQQQQ<CR>')
  330. screen:expect({
  331. grid = [[
  332. {9:obal 'err' (a nil value)} |
  333. {9:stack traceback:} |
  334. {9: [string "<nvim>"]:2: in function}|
  335. {9: <[string "<nvim>"]:1>} |
  336. {100:Press ENTER or type command to continue}^ |
  337. ]],
  338. messages = {
  339. {
  340. content = { { 'Press ENTER or type command to continue', 100, 18 } },
  341. history = true,
  342. kind = 'return_prompt',
  343. },
  344. },
  345. })
  346. feed(':1mes clear<CR>:mes<CR>')
  347. screen:expect([[
  348. |
  349. {3: }|
  350. {9:Excessive errors in vim.ui_attach() call}|
  351. {9:back from ns: 1.} |
  352. {100:Press ENTER or type command to continue}^ |
  353. ]])
  354. feed('<cr>')
  355. -- Also when scheduled
  356. exec_lua([[
  357. vim.ui_attach(vim.api.nvim_create_namespace(''), { ext_messages = true }, function()
  358. vim.schedule(function() vim.api.nvim_buf_set_lines(0, -2, -1, false, { err[1] }) end)
  359. end)
  360. ]])
  361. screen:expect({
  362. grid = s1,
  363. messages = {
  364. {
  365. content = {
  366. {
  367. 'Error executing vim.schedule lua callback: [string "<nvim>"]:2: attempt to index global \'err\' (a nil value)\nstack traceback:\n\t[string "<nvim>"]:2: in function <[string "<nvim>"]:2>',
  368. 9,
  369. 6,
  370. },
  371. },
  372. history = true,
  373. kind = 'lua_error',
  374. },
  375. {
  376. content = {
  377. {
  378. 'Error executing vim.schedule lua callback: [string "<nvim>"]:2: attempt to index global \'err\' (a nil value)\nstack traceback:\n\t[string "<nvim>"]:2: in function <[string "<nvim>"]:2>',
  379. 9,
  380. 6,
  381. },
  382. },
  383. history = true,
  384. kind = 'lua_error',
  385. },
  386. {
  387. content = { { 'Press ENTER or type command to continue', 100, 18 } },
  388. history = false,
  389. kind = 'return_prompt',
  390. },
  391. },
  392. })
  393. feed('<esc>:1mes clear<cr>:mes<cr>')
  394. screen:expect([[
  395. |
  396. {3: }|
  397. {9:Excessive errors in vim.ui_attach() call}|
  398. {9:back from ns: 2.} |
  399. {100:Press ENTER or type command to continue}^ |
  400. ]])
  401. end)
  402. it('sourcing invalid file does not crash #32166', function()
  403. exec_lua([[
  404. local ns = vim.api.nvim_create_namespace("")
  405. vim.ui_attach(ns, { ext_messages = true }, function() end)
  406. ]])
  407. feed((':luafile %s<CR>'):format(testlog))
  408. n.assert_alive()
  409. end)
  410. end)