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