buffer_spec.lua (63913B)
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 assert_alive = n.assert_alive 7 local feed, clear = n.feed, n.clear 8 local poke_eventloop = n.poke_eventloop 9 local nvim_prog = n.nvim_prog 10 local eval, source = n.eval, n.source 11 local pcall_err = t.pcall_err 12 local eq, neq = t.eq, t.neq 13 local api = n.api 14 local retry = t.retry 15 local testprg = n.testprg 16 local write_file = t.write_file 17 local command = n.command 18 local matches = t.matches 19 local exec_lua = n.exec_lua 20 local sleep = vim.uv.sleep 21 local fn = n.fn 22 local is_os = t.is_os 23 local skip = t.skip 24 25 describe(':terminal buffer', function() 26 local screen 27 28 before_each(function() 29 clear() 30 command('set modifiable swapfile undolevels=20') 31 screen = tt.setup_screen() 32 end) 33 34 it('terminal-mode forces various options', function() 35 local expr = 36 '[&l:cursorlineopt, &l:cursorline, &l:cursorcolumn, &l:scrolloff, &l:sidescrolloff]' 37 38 feed([[<C-\><C-N>]]) 39 command('setlocal cursorline cursorlineopt=both cursorcolumn scrolloff=4 sidescrolloff=7') 40 eq({ 'both', 1, 1, 4, 7 }, eval(expr)) 41 eq('nt', eval('mode(1)')) 42 43 -- Enter Terminal mode ("insert" mode in :terminal). 44 feed('i') 45 eq('t', eval('mode(1)')) 46 eq({ 'number', 1, 0, 0, 0 }, eval(expr)) 47 48 -- Return to Normal mode. 49 feed([[<C-\><C-N>]]) 50 eq('nt', eval('mode(1)')) 51 eq({ 'both', 1, 1, 4, 7 }, eval(expr)) 52 53 -- Enter Terminal mode again. 54 feed('i') 55 eq('t', eval('mode(1)')) 56 eq({ 'number', 1, 0, 0, 0 }, eval(expr)) 57 58 -- Delete the terminal buffer and return to the previous buffer. 59 command('bwipe!') 60 feed('<Ignore>') -- Add input to separate two RPC requests 61 eq('n', eval('mode(1)')) 62 -- Window options in the old buffer should be unchanged. #37484 63 eq({ 'both', 0, 0, -1, -1 }, eval(expr)) 64 end) 65 66 it('terminal-mode does not change cursorlineopt if cursorline is disabled', function() 67 feed([[<C-\><C-N>]]) 68 command('setlocal nocursorline cursorlineopt=both') 69 feed('i') 70 eq({ 0, 'both' }, eval('[&l:cursorline, &l:cursorlineopt]')) 71 end) 72 73 it('terminal-mode disables cursorline when cursorlineopt is only set to "line"', function() 74 feed([[<C-\><C-N>]]) 75 command('setlocal cursorline cursorlineopt=line') 76 feed('i') 77 eq({ 0, 'line' }, eval('[&l:cursorline, &l:cursorlineopt]')) 78 end) 79 80 describe('swap and undo', function() 81 before_each(function() 82 feed('<c-\\><c-n>') 83 screen:expect([[ 84 tty ready | 85 ^ | 86 |*5 87 ]]) 88 end) 89 90 it('does not create swap files', function() 91 eq('No swap file', n.exec_capture('swapname')) 92 end) 93 94 it('does not create undo files', function() 95 local undofile = api.nvim_eval('undofile(bufname("%"))') 96 eq(nil, io.open(undofile)) 97 end) 98 end) 99 100 it('cannot be modified directly', function() 101 feed('<c-\\><c-n>dd') 102 screen:expect([[ 103 tty ready | 104 ^ | 105 |*4 106 {101:E21: Cannot make changes, 'modifiable' is off} | 107 ]]) 108 end) 109 110 it('sends data to the terminal when the "put" operator is used', function() 111 feed('<c-\\><c-n>gg"ayj') 112 fn.setreg('a', 'appended ' .. fn.getreg('a')) 113 feed('"ap"ap') 114 screen:expect([[ 115 ^tty ready | 116 appended tty ready |*2 117 |*4 118 ]]) 119 -- operator count is also taken into consideration 120 feed('3"ap') 121 screen:expect([[ 122 ^tty ready | 123 appended tty ready |*5 124 | 125 ]]) 126 end) 127 128 it('sends data to the terminal when the ":put" command is used', function() 129 feed('<c-\\><c-n>gg"ayj') 130 fn.setreg('a', 'appended ' .. fn.getreg('a')) 131 feed(':put a<CR>') 132 screen:expect([[ 133 ^tty ready | 134 appended tty ready | 135 |*4 136 :put a | 137 ]]) 138 -- line argument is only used to move the cursor 139 feed(':6put a<CR>') 140 screen:expect([[ 141 tty ready | 142 appended tty ready |*2 143 |*2 144 ^ | 145 :6put a | 146 ]]) 147 end) 148 149 it('can be deleted', function() 150 feed('<c-\\><c-n>:bd!<cr>') 151 screen:expect([[ 152 ^ | 153 {100:~ }|*5 154 :bd! | 155 ]]) 156 feed(':bnext<CR>') 157 screen:expect([[ 158 ^ | 159 {100:~ }|*5 160 :bnext | 161 ]]) 162 end) 163 164 it('handles loss of focus gracefully', function() 165 -- Change the statusline to avoid printing the file name, which varies. 166 api.nvim_set_option_value('statusline', '==========', {}) 167 168 -- Save the buffer number of the terminal for later testing. 169 local tbuf = eval('bufnr("%")') 170 local exitcmd = is_os('win') and "['cmd', '/c', 'exit']" or "['sh', '-c', 'exit']" 171 source([[ 172 function! SplitWindow(id, data, event) 173 new 174 call feedkeys("iabc\<Esc>") 175 endfunction 176 177 startinsert 178 call jobstart(]] .. exitcmd .. [[, {'on_exit': function("SplitWindow")}) 179 call feedkeys("\<C-\>", 't') " vim will expect <C-n>, but be exited out of 180 " the terminal before it can be entered. 181 ]]) 182 183 -- We should be in a new buffer now. 184 screen:expect([[ 185 ab^c | 186 {100:~ }| 187 {3:========== }| 188 rows: 2, cols: 50 | 189 | 190 {119:========== }| 191 | 192 ]]) 193 194 neq(tbuf, eval('bufnr("%")')) 195 command('quit!') -- Should exit the new window, not the terminal. 196 eq(tbuf, eval('bufnr("%")')) 197 end) 198 199 describe('handles confirmations', function() 200 it('with :confirm', function() 201 feed('<c-\\><c-n>') 202 feed(':confirm bdelete<CR>') 203 screen:expect { any = 'Close "term://' } 204 end) 205 206 it('with &confirm', function() 207 feed('<c-\\><c-n>') 208 feed(':bdelete<CR>') 209 screen:expect { any = 'E89' } 210 feed('<cr>') 211 eq('terminal', eval('&buftype')) 212 feed(':set confirm | bdelete<CR>') 213 screen:expect { any = 'Close "term://' } 214 feed('y') 215 neq('terminal', eval('&buftype')) 216 end) 217 end) 218 219 it('it works with set rightleft #11438', function() 220 local columns = eval('&columns') 221 feed(string.rep('a', columns)) 222 command('set rightleft') 223 screen:expect([[ 224 ydaer ytt| 225 ^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa| 226 |*4 227 {5:-- TERMINAL --} | 228 ]]) 229 command('bdelete!') 230 end) 231 232 it('requires bang (!) to close a running job #15402', function() 233 eq('Vim(wqall):E948: Job still running (add ! to end the job)', pcall_err(command, 'wqall')) 234 for _, cmd in ipairs({ 'bdelete', '%bdelete', 'bwipeout', 'bunload' }) do 235 matches( 236 '^Vim%(' 237 .. cmd:gsub('%%', '') 238 .. '%):E89: term://.*tty%-test.* will be killed %(add %! to override%)$', 239 pcall_err(command, cmd) 240 ) 241 end 242 command('call jobstop(&channel)') 243 assert(0 >= eval('jobwait([&channel], 1000)[0]')) 244 command('bdelete') 245 end) 246 247 it(':wqall! closes a running job', function() 248 n.expect_exit(command, 'wqall!') 249 end) 250 251 it('stops running jobs with :quit', function() 252 -- Open in a new window to avoid terminating the nvim instance 253 command('split') 254 command('terminal') 255 command('set nohidden') 256 command('quit') 257 end) 258 259 it('does not segfault when pasting empty register #13955', function() 260 feed('<c-\\><c-n>') 261 command('put a') -- register a is empty 262 n.assert_alive() 263 end) 264 265 it([[can use temporary normal mode <c-\><c-o>]], function() 266 eq('t', fn.mode(1)) 267 feed [[<c-\><c-o>]] 268 screen:expect([[ 269 tty ready | 270 ^ | 271 |*4 272 {5:-- (terminal) --} | 273 ]]) 274 eq('ntT', fn.mode(1)) 275 276 feed [[:let g:x = 17]] 277 screen:expect([[ 278 tty ready | 279 |*5 280 :let g:x = 17^ | 281 ]]) 282 283 feed [[<cr>]] 284 screen:expect([[ 285 tty ready | 286 ^ | 287 |*4 288 {5:-- TERMINAL --} | 289 ]]) 290 eq('t', fn.mode(1)) 291 end) 292 293 it('is refreshed with partial mappings in Terminal mode #9167', function() 294 command([[set timeoutlen=20000 | tnoremap jk <C-\><C-N>]]) 295 feed('j') -- Won't reach the terminal until the next character is typed 296 screen:expect_unchanged() 297 feed('j') -- Refresh scheduled for the first 'j' but not processed 298 screen:expect_unchanged() 299 for i = 1, 10 do 300 eq({ mode = 't', blocking = true }, api.nvim_get_mode()) 301 vim.uv.sleep(15) -- Wait for the previously scheduled refresh timer to arrive 302 feed('j') -- Refresh scheduled for the last 'j' and processed for the one before 303 screen:expect(([[ 304 tty ready | 305 %s^%s| 306 |*4 307 {5:-- TERMINAL --} | 308 ]]):format(('j'):rep(i), (' '):rep(50 - i))) 309 end 310 feed('l') -- No partial mapping, so all pending refreshes should be processed 311 screen:expect([[ 312 tty ready | 313 jjjjjjjjjjjjl^ | 314 |*4 315 {5:-- TERMINAL --} | 316 ]]) 317 end) 318 319 it('is refreshed with partial mappings in Normal mode', function() 320 command('set timeoutlen=20000 | nnoremap jk :') 321 command('nnoremap j <Cmd>call chansend(&channel, "j")<CR>') 322 feed([[<C-\><C-N>]]) 323 screen:expect([[ 324 tty ready | 325 ^ | 326 |*5 327 ]]) 328 feed('j') -- Won't reach the terminal until the next character is typed 329 screen:expect_unchanged() 330 feed('j') -- Refresh scheduled for the first 'j' but not processed 331 screen:expect_unchanged() 332 for i = 1, 10 do 333 eq({ mode = 'nt', blocking = true }, api.nvim_get_mode()) 334 vim.uv.sleep(15) -- Wait for the previously scheduled refresh timer to arrive 335 feed('j') -- Refresh scheduled for the last 'j' and processed for the one before 336 screen:expect(([[ 337 tty ready | 338 ^%s%s| 339 |*5 340 ]]):format(('j'):rep(i), (' '):rep(50 - i))) 341 end 342 feed('l') -- No partial mapping, so all pending refreshes should be processed 343 screen:expect([[ 344 tty ready | 345 j^jjjjjjjjjjj | 346 |*5 347 ]]) 348 end) 349 350 it('writing to an existing file with :w fails #13549', function() 351 eq( 352 'Vim(write):E13: File exists (add ! to override)', 353 pcall_err(command, 'write test/functional/fixtures/tty-test.c') 354 ) 355 end) 356 357 it('external interrupt (got_int) does not hang #20726', function() 358 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 359 command('call timer_start(0, {-> interrupt()})') 360 feed('<Ignore>') -- Add input to separate two RPC requests 361 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 362 feed([[<C-\><C-N>]]) 363 eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) 364 command('bd!') 365 end) 366 367 it('correct size when switching buffers', function() 368 local term_buf = api.nvim_get_current_buf() 369 command('file foo | enew | vsplit') 370 api.nvim_set_current_buf(term_buf) 371 screen:expect([[ 372 tty ready │ | 373 ^rows: 5, cols: 25 │{100:~ }| 374 │{100:~ }|*3 375 {120:foo [-] }{2:[No Name] }| 376 | 377 ]]) 378 379 feed('<C-^><C-W><C-O><C-^>') 380 screen:expect([[ 381 tty ready | 382 ^rows: 5, cols: 25 | 383 rows: 6, cols: 50 | 384 |*4 385 ]]) 386 end) 387 388 it('reports focus notifications when requested', function() 389 feed([[<C-\><C-N>]]) 390 exec_lua(function() 391 local function new_test_term() 392 local chan = vim.api.nvim_open_term(0, { 393 on_input = function(_, term, buf, data) 394 if data == '\27[I' then 395 vim.b[buf].term_focused = true 396 vim.api.nvim_chan_send(term, 'focused\n') 397 elseif data == '\27[O' then 398 vim.b[buf].term_focused = false 399 vim.api.nvim_chan_send(term, 'unfocused\n') 400 end 401 end, 402 }) 403 vim.b.term_focused = false 404 vim.api.nvim_chan_send(chan, '\27[?1004h') -- Enable focus reporting 405 end 406 407 vim.cmd 'edit bar' 408 new_test_term() 409 vim.cmd 'vnew foo' 410 new_test_term() 411 vim.cmd 'vsplit' 412 end) 413 screen:expect([[ 414 ^ │ │ | 415 │ │ |*4 416 {120:foo [-] }{119:foo [-] bar [-] }| 417 | 418 ]]) 419 420 -- TermEnter/Leave happens *after* entering/leaving terminal mode, so focus should've changed 421 -- already by the time these events run. 422 exec_lua(function() 423 _G.last_event = nil 424 vim.api.nvim_create_autocmd({ 'TermEnter', 'TermLeave' }, { 425 callback = function(args) 426 _G.last_event = args.event 427 .. ' ' 428 .. vim.fs.basename(args.file) 429 .. ' ' 430 .. tostring(vim.b[args.buf].term_focused) 431 end, 432 }) 433 end) 434 435 feed('i') 436 screen:expect([[ 437 focused │focused │ | 438 ^ │ │ | 439 │ │ |*3 440 {120:foo [-] }{119:foo [-] bar [-] }| 441 {5:-- TERMINAL --} | 442 ]]) 443 eq('TermEnter foo true', exec_lua('return _G.last_event')) 444 445 -- Next window has the same terminal; no new notifications. 446 command('wincmd w') 447 screen:expect([[ 448 focused │focused │ | 449 │^ │ | 450 │ │ |*3 451 {119:foo [-] }{120:foo [-] }{119:bar [-] }| 452 {5:-- TERMINAL --} | 453 ]]) 454 -- Next window has a different terminal; expect new unfocus and focus notifications. 455 command('wincmd w') 456 screen:expect([[ 457 focused │focused │focused | 458 unfocused │unfocuse│^ | 459 │ │ |*3 460 {119:foo [-] foo [-] }{120:bar [-] }| 461 {5:-- TERMINAL --} | 462 ]]) 463 -- Leaving terminal mode; expect a new unfocus notification. 464 feed([[<C-\><C-N>]]) 465 screen:expect([[ 466 focused │focused │focused | 467 unfocused │unfocuse│unfocused | 468 │ │^ | 469 │ │ |*2 470 {119:foo [-] foo [-] }{120:bar [-] }| 471 | 472 ]]) 473 eq('TermLeave bar false', exec_lua('return _G.last_event')) 474 end) 475 476 it('no crash with race between buffer close and OSC 2', function() 477 skip(is_os('win'), 'tty-test cannot forward OSC 2 on Windows?') 478 exec_lua(function() 479 vim.api.nvim_chan_send(vim.bo.channel, '\027]2;SOME_TITLE\007') 480 end) 481 retry(nil, 4000, function() 482 eq('SOME_TITLE', api.nvim_buf_get_var(0, 'term_title')) 483 end) 484 screen:expect_unchanged() 485 --- @type string 486 local title_before_del = exec_lua(function() 487 vim.wait(10) -- Ensure there are no pending events so that a write isn't queued. 488 vim.api.nvim_chan_send(vim.bo.channel, '\027]2;OTHER_TITLE\007') 489 vim.uv.sleep(50) -- Block the event loop and wait for tty-test to forward OSC 2. 490 local term_title = vim.b.term_title 491 vim.api.nvim_buf_delete(0, { force = true }) 492 vim.wait(10, nil, nil, true) -- Process fast events only. 493 return term_title 494 end) 495 -- Title isn't changed until the second vim.wait(). 496 eq('SOME_TITLE', title_before_del) 497 screen:expect([[ 498 ^ | 499 {100:~ }|*5 500 | 501 ]]) 502 assert_alive() 503 end) 504 505 describe('handles suspended PTY process', function() 506 if skip(is_os('win'), 'N/A for Windows') then 507 return 508 end 509 510 --- @param external_resume boolean 511 local function test_term_process_suspend_resume(external_resume) 512 command('set mousemodel=extend') 513 local pid = eval('jobpid(&channel)') 514 vim.uv.kill(pid, 'sigstop') 515 -- In Terminal mode the cursor is at the "[Process suspended]" text to hint that 516 -- pressing a key will change the suspended state. 517 local s0 = [[ 518 tty ready | 519 |*4 520 ^[Process suspended] | 521 {5:-- TERMINAL --} | 522 ]] 523 screen:expect(s0) 524 feed([[<C-\><C-N>]]) 525 -- Returning to Normal mode puts the cursor at its previous position as that's 526 -- closer to the terminal output, making it easier for the user to copy. 527 screen:expect([[ 528 tty ready | 529 ^ | 530 |*3 531 [Process suspended] | 532 | 533 ]]) 534 feed('i') 535 screen:expect(s0) 536 command('set laststatus=0 | botright vsplit') 537 screen:expect([[ 538 tty ready │tty ready | 539 │ |*4 540 [Process suspended] │^[Process suspended] | 541 {5:-- TERMINAL --} | 542 ]]) 543 -- Resize is detected by the process on resume. 544 if external_resume then 545 vim.uv.kill(pid, 'sigcont') 546 else 547 feed('a') 548 end 549 screen:expect([[ 550 tty ready │tty ready | 551 rows: 6, cols: 25 │rows: 6, cols: 25 | 552 │^ | 553 │ |*3 554 {5:-- TERMINAL --} | 555 ]]) 556 tt.enable_mouse() 557 tt.feed_data('mouse enabled\n\n\n\n') 558 screen:expect([[ 559 rows: 6, cols: 25 │rows: 6, cols: 25 | 560 mouse enabled │mouse enabled | 561 │ |*3 562 │^ | 563 {5:-- TERMINAL --} | 564 ]]) 565 api.nvim_input_mouse('right', 'press', '', 0, 0, 25) 566 screen:expect({ any = vim.pesc('"!!^') }) 567 api.nvim_input_mouse('right', 'release', '', 0, 0, 25) 568 screen:expect({ any = vim.pesc('#!!^') }) 569 vim.uv.kill(pid, 'sigstop') 570 local s1 = [[ 571 rows: 6, cols: 25 │rows: 6, cols: 25 | 572 mouse enabled │mouse enabled | 573 │ |*3 574 [Process suspended] │^[Process suspended] | 575 {5:-- TERMINAL --} | 576 ]] 577 screen:expect(s1) 578 -- Mouse isn't forwarded when process is suspended. 579 api.nvim_input_mouse('right', 'press', '', 0, 1, 27) 580 api.nvim_input_mouse('right', 'release', '', 0, 1, 27) 581 screen:expect([[ 582 rows: 6, cols: 25 │rows: 6, cols: 25 | 583 mo{108:use enabled} │mo^u{108:se enabled} | 584 {108: } │{108: } |*3 585 [Process suspended] │[Process suspended] | 586 {5:-- VISUAL --} | 587 ]]) 588 feed('<Esc>i') 589 screen:expect(s1) 590 if external_resume then 591 vim.uv.kill(pid, 'sigcont') 592 else 593 feed('a') 594 end 595 screen:expect([[ 596 rows: 6, cols: 25 │rows: 6, cols: 25 | 597 mouse enabled │mouse enabled | 598 │ |*3 599 #!! │ #!!^ | 600 {5:-- TERMINAL --} | 601 ]]) 602 -- Mouse is forwarded after process is resumed. 603 api.nvim_input_mouse('right', 'press', '', 0, 0, 28) 604 screen:expect({ any = vim.pesc('"$!^') }) 605 api.nvim_input_mouse('right', 'release', '', 0, 0, 28) 606 screen:expect({ any = vim.pesc('#$!^') }) 607 end 608 609 it('resumed by an external signal', function() 610 skip(is_os('mac'), 'FIXME: does not work on macOS') 611 test_term_process_suspend_resume(true) 612 end) 613 614 it('resumed by pressing a key', function() 615 test_term_process_suspend_resume(false) 616 end) 617 end) 618 end) 619 620 describe(':terminal buffer', function() 621 before_each(clear) 622 623 it('can resume suspended PTY process running in fish', function() 624 skip(is_os('win'), 'N/A for Windows') 625 skip(fn.executable('fish') == 0, 'missing "fish" command') 626 627 local screen = Screen.new(50, 7) 628 screen:add_extra_attr_ids({ 629 [100] = { 630 foreground = Screen.colors.NvimDarkGrey2, 631 background = Screen.colors.NvimLightGrey2, 632 }, 633 [101] = { 634 foreground = Screen.colors.NvimLightGrey4, 635 background = Screen.colors.NvimLightGrey2, 636 }, 637 [102] = { 638 foreground = Screen.colors.NvimDarkGrey2, 639 background = Screen.colors.NvimLightGrey4, 640 }, 641 }) 642 command('set shell=fish termguicolors') 643 command(('terminal %s -u NONE -i NONE'):format(fn.shellescape(nvim_prog))) 644 command('startinsert') 645 local s0 = [[ 646 {100:^ }| 647 {101:~ }|*3 648 {102:[No Name] 0,0-1 All}| 649 {100: }| 650 {5:-- TERMINAL --} | 651 ]] 652 screen:expect(s0) 653 feed('<C-Z>') 654 screen:expect([[ 655 |*5 656 ^[Process suspended] | 657 {5:-- TERMINAL --} | 658 ]]) 659 feed('<Space>') 660 screen:expect(s0) 661 end) 662 663 it('term_close() use-after-free #4393', function() 664 command('terminal yes') 665 feed('<Ignore>') -- Add input to separate two RPC requests 666 command('bdelete!') 667 end) 668 669 describe('TermRequest', function() 670 it('emits events #26972', function() 671 local term = api.nvim_open_term(0, {}) 672 local termbuf = api.nvim_get_current_buf() 673 674 -- Test that <abuf> is the terminal buffer, not the current buffer 675 command('au TermRequest * let g:termbuf = +expand("<abuf>")') 676 command('wincmd p') 677 678 -- cwd will be inserted in a file URI, which cannot contain backs 679 local cwd = t.fix_slashes(fn.getcwd()) 680 local parent = cwd:match('^(.+/)') 681 local expected = '\027]7;file://host' .. parent 682 api.nvim_chan_send(term, string.format('%s\027\\', expected)) 683 eq(expected, eval('v:termrequest')) 684 eq(termbuf, eval('g:termbuf')) 685 end) 686 687 it('emits events for APC', function() 688 local term = api.nvim_open_term(0, {}) 689 690 -- cwd will be inserted in a file URI, which cannot contain backs 691 local cwd = t.fix_slashes(fn.getcwd()) 692 local parent = cwd:match('^(.+/)') 693 local expected = '\027_Gfile://host' .. parent 694 api.nvim_chan_send(term, string.format('%s\027\\', expected)) 695 eq(expected, eval('v:termrequest')) 696 end) 697 698 it('synchronization #27572', function() 699 command('autocmd! nvim.terminal TermRequest') 700 local term = exec_lua([[ 701 _G.input = {} 702 local term = vim.api.nvim_open_term(0, { 703 on_input = function(_, _, _, data) 704 table.insert(_G.input, data) 705 end, 706 force_crlf = false, 707 }) 708 vim.api.nvim_create_autocmd('TermRequest', { 709 callback = function(args) 710 if args.data.sequence == '\027]11;?' then 711 table.insert(_G.input, '\027]11;rgb:0000/0000/0000\027\\') 712 end 713 end 714 }) 715 return term 716 ]]) 717 api.nvim_chan_send(term, '\027]11;?\007\027[5n\027]11;?\007\027[5n') 718 eq({ 719 '\027]11;rgb:0000/0000/0000\027\\', 720 '\027[0n', 721 '\027]11;rgb:0000/0000/0000\027\\', 722 '\027[0n', 723 }, exec_lua('return _G.input')) 724 end) 725 726 it('works with vim.wait() from another autocommand #32706', function() 727 command('autocmd! nvim.terminal TermRequest') 728 exec_lua([[ 729 local term = vim.api.nvim_open_term(0, {}) 730 vim.api.nvim_create_autocmd('TermRequest', { 731 buffer = 0, 732 callback = function(ev) 733 _G.sequence = ev.data.sequence 734 _G.v_termrequest = vim.v.termrequest 735 end, 736 }) 737 vim.api.nvim_create_autocmd('TermEnter', { 738 buffer = 0, 739 callback = function() 740 vim.api.nvim_chan_send(term, '\027]11;?\027\\') 741 _G.result = vim.wait(3000, function() 742 local expected = '\027]11;?' 743 return _G.sequence == expected and _G.v_termrequest == expected 744 end) 745 end, 746 }) 747 ]]) 748 feed('i') 749 retry(nil, 4000, function() 750 eq(true, exec_lua('return _G.result')) 751 end) 752 end) 753 754 it('includes cursor position #31609', function() 755 command('autocmd! nvim.terminal TermRequest') 756 local screen = Screen.new(50, 10) 757 local term = exec_lua([[ 758 _G.cursor = {} 759 local term = vim.api.nvim_open_term(0, {}) 760 vim.api.nvim_create_autocmd('TermRequest', { 761 callback = function(args) 762 _G.cursor = args.data.cursor 763 end 764 }) 765 return term 766 ]]) 767 -- Enter terminal mode so that the cursor follows the output 768 feed('a') 769 770 -- Put some lines into the scrollback. This tests the conversion from terminal line to buffer 771 -- line. 772 api.nvim_chan_send(term, string.rep('>\n', 20)) 773 screen:expect([[ 774 > |*8 775 ^ | 776 {5:-- TERMINAL --} | 777 ]]) 778 779 -- Emit an OSC escape sequence 780 api.nvim_chan_send(term, 'Hello\nworld!\027]133;D\027\\') 781 screen:expect([[ 782 > |*7 783 Hello | 784 world!^ | 785 {5:-- TERMINAL --} | 786 ]]) 787 eq({ 22, 6 }, exec_lua('return _G.cursor')) 788 789 api.nvim_chan_send(term, '\nHello\027]133;D\027\\\nworld!\n') 790 screen:expect([[ 791 > |*4 792 Hello | 793 world! | 794 Hello | 795 world! | 796 ^ | 797 {5:-- TERMINAL --} | 798 ]]) 799 eq({ 23, 5 }, exec_lua('return _G.cursor')) 800 801 api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(6)) 802 screen:expect([[ 803 world! | 804 Hello | 805 world! | 806 |*5 807 ^ | 808 {5:-- TERMINAL --} | 809 ]]) 810 eq({ 25, 5 }, exec_lua('return _G.cursor')) 811 812 api.nvim_set_option_value('scrollback', 10, {}) 813 eq(19, api.nvim_buf_line_count(0)) 814 815 api.nvim_chan_send(term, 'Hello\nworld!\027]133;D\027\\') 816 screen:expect([[ 817 Hello | 818 world! | 819 |*5 820 Hello | 821 world!^ | 822 {5:-- TERMINAL --} | 823 ]]) 824 eq({ 19, 6 }, exec_lua('return _G.cursor')) 825 826 api.nvim_chan_send(term, '\nHello\027]133;D\027\\\nworld!\n') 827 screen:expect([[ 828 |*4 829 Hello | 830 world! | 831 Hello | 832 world! | 833 ^ | 834 {5:-- TERMINAL --} | 835 ]]) 836 eq({ 17, 5 }, exec_lua('return _G.cursor')) 837 838 api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(6)) 839 screen:expect([[ 840 world! | 841 Hello | 842 world! | 843 |*5 844 ^ | 845 {5:-- TERMINAL --} | 846 ]]) 847 eq({ 12, 5 }, exec_lua('return _G.cursor')) 848 849 api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(8)) 850 screen:expect([[ 851 world! | 852 |*7 853 ^ | 854 {5:-- TERMINAL --} | 855 ]]) 856 eq({ 10, 5 }, exec_lua('return _G.cursor')) 857 858 api.nvim_chan_send(term, 'Hello\027]133;D\027\\\nworld!' .. ('\n'):rep(20)) 859 screen:expect([[ 860 |*8 861 ^ | 862 {5:-- TERMINAL --} | 863 ]]) 864 eq({ -2, 5 }, exec_lua('return _G.cursor')) 865 end) 866 867 it('does not cause hang in vim.wait() #32753', function() 868 local screen = Screen.new(50, 10) 869 870 exec_lua(function() 871 local term = vim.api.nvim_open_term(0, {}) 872 873 -- Write OSC sequence with pending scrollback. TermRequest will 874 -- reschedule itself onto an event queue until the pending scrollback is 875 -- processed (i.e. the terminal is refreshed). 876 vim.api.nvim_chan_send(term, string.format('%s\027]133;;\007', string.rep('a\n', 100))) 877 878 -- vim.wait() drains the event queue. The terminal won't be refreshed 879 -- until the event queue is empty. This test ensures that TermRequest 880 -- does not continuously reschedule itself onto the same event queue, 881 -- causing an infinite loop. 882 vim.wait(100) 883 end) 884 885 screen:expect([[ 886 ^a | 887 a |*8 888 | 889 ]]) 890 end) 891 892 describe('no heap-use-after-free after', function() 893 local term 894 895 before_each(function() 896 term = exec_lua(function() 897 vim.api.nvim_create_autocmd('TermRequest', { callback = function() end }) 898 return vim.api.nvim_open_term(0, {}) 899 end) 900 end) 901 902 it('wiping buffer with pending TermRequest #37226', function() 903 exec_lua(function() 904 vim.api.nvim_chan_send(term, '\027]8;;https://example.com\027\\') 905 vim.api.nvim_buf_delete(0, { force = true }) 906 end) 907 assert_alive() 908 end) 909 910 it('unloading buffer with pending TermRequest #37226', function() 911 api.nvim_create_buf(true, false) -- Create a buffer to switch to. 912 exec_lua(function() 913 vim.api.nvim_chan_send(term, '\027]8;;https://example.com\027\\') 914 vim.api.nvim_buf_delete(0, { force = true, unload = true }) 915 end) 916 assert_alive() 917 end) 918 end) 919 end) 920 921 it('no heap-buffer-overflow when using jobstart("echo",{term=true}) #3161', function() 922 local testfilename = 'Xtestfile-functional-terminal-buffers_spec' 923 write_file(testfilename, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa') 924 finally(function() 925 os.remove(testfilename) 926 end) 927 feed(':edit ' .. testfilename .. '<CR>') 928 -- Move cursor away from the beginning of the line 929 feed('$') 930 -- Let jobstart(…,{term=true}) modify the buffer 931 fn.jobstart('echo', { term = true }) 932 assert_alive() 933 command('bdelete!') 934 end) 935 936 it('no heap-buffer-overflow when sending long line with nowrap #11548', function() 937 command('set nowrap | autocmd TermOpen * startinsert') 938 feed(':call feedkeys("4000ai\\<esc>:terminal!\\<cr>")<CR>') 939 assert_alive() 940 end) 941 942 it('truncates the size of grapheme clusters', function() 943 local chan = api.nvim_open_term(0, {}) 944 local composing = ('a̳'):sub(2) 945 api.nvim_chan_send(chan, 'a' .. composing:rep(20)) 946 retry(nil, nil, function() 947 eq('a' .. composing:rep(14), api.nvim_get_current_line()) 948 end) 949 end) 950 951 it('handles extended grapheme clusters', function() 952 local screen = Screen.new(50, 7) 953 feed 'i' 954 local chan = api.nvim_open_term(0, {}) 955 api.nvim_chan_send(chan, '🏴☠️ yarrr') 956 screen:expect([[ 957 🏴☠️ yarrr^ | 958 |*5 959 {5:-- TERMINAL --} | 960 ]]) 961 eq('🏴☠️ yarrr', api.nvim_get_current_line()) 962 end) 963 964 it('handles split UTF-8 sequences #16245', function() 965 local screen = Screen.new(50, 10) 966 fn.jobstart({ testprg('shell-test'), 'UTF-8' }, { term = true }) 967 screen:expect([[ 968 ^å | 969 ref: å̲ | 970 1: å̲ | 971 2: å̲ | 972 3: å̲ | 973 | 974 [Process exited 0] | 975 |*3 976 ]]) 977 -- Test with Unicode char at right edge using a 4-wide terminal 978 command('bwipe! | set laststatus=0 | 4vnew') 979 fn.jobstart({ testprg('shell-test'), 'UTF-8' }, { term = true }) 980 screen:expect([[ 981 ^å │ | 982 ref:│{1:~ }| 983 å̲ │{1:~ }| 984 1: å̲│{1:~ }| 985 2: å̲│{1:~ }| 986 3: å̲│{1:~ }| 987 │{1:~ }| 988 [Pro│{1:~ }| 989 cess│{1:~ }| 990 | 991 ]]) 992 end) 993 994 --- @param subcmd 'REP'|'REPFAST' 995 local function check_term_rep(subcmd, count) 996 local screen = Screen.new(50, 7) 997 api.nvim_create_autocmd('TermClose', { command = 'let g:did_termclose = 1' }) 998 fn.jobstart({ testprg('shell-test'), subcmd, count, 'TEST' }, { term = true }) 999 retry(nil, nil, function() 1000 eq(1, api.nvim_get_var('did_termclose')) 1001 end) 1002 feed('i') 1003 screen:expect(([[ 1004 %d: TEST{MATCH: +}| 1005 %d: TEST{MATCH: +}| 1006 %d: TEST{MATCH: +}| 1007 %d: TEST{MATCH: +}| 1008 | 1009 [Process exited 0]^ | 1010 {5:-- TERMINAL --} | 1011 ]]):format(count - 4, count - 3, count - 2, count - 1)) 1012 local lines = api.nvim_buf_get_lines(0, 0, -1, true) 1013 for i = 1, count do 1014 eq(('%d: TEST'):format(i - 1), lines[i]) 1015 end 1016 end 1017 1018 it('does not drop data when job exits immediately after output #3030', function() 1019 api.nvim_set_option_value('scrollback', 30000, {}) 1020 check_term_rep('REPFAST', 20000) 1021 end) 1022 1023 it('does not drop data when autocommands poll for events #37559', function() 1024 api.nvim_set_option_value('scrollback', 30000, {}) 1025 api.nvim_create_autocmd('BufFilePre', { command = 'sleep 50m', nested = true }) 1026 api.nvim_create_autocmd('BufFilePost', { command = 'sleep 50m', nested = true }) 1027 api.nvim_create_autocmd('TermOpen', { command = 'sleep 50m', nested = true }) 1028 -- REP pauses 1 ms every 100 lines, so each autocommand processes some output. 1029 check_term_rep('REP', 20000) 1030 end) 1031 1032 describe('scrollback is correct if all output is drained by', function() 1033 for _, event in ipairs({ 'BufFilePre', 'BufFilePost', 'TermOpen' }) do 1034 describe(('%s autocommand that lasts for'):format(event), function() 1035 for _, delay in ipairs({ 5, 15, 25 }) do 1036 -- Terminal refresh delay is 10 ms. 1037 it(('%.1f * terminal refresh delay'):format(delay / 10), function() 1038 local cmd = ('sleep %dm'):format(delay) 1039 api.nvim_create_autocmd(event, { command = cmd, nested = true }) 1040 check_term_rep('REPFAST', 200) 1041 end) 1042 end 1043 end) 1044 end 1045 end) 1046 1047 it('handles unprintable chars', function() 1048 local screen = Screen.new(50, 7) 1049 feed 'i' 1050 local chan = api.nvim_open_term(0, {}) 1051 api.nvim_chan_send(chan, '\239\187\191') -- '\xef\xbb\xbf' 1052 screen:expect([[ 1053 {18:<feff>}^ | 1054 |*5 1055 {5:-- TERMINAL --} | 1056 ]]) 1057 eq('\239\187\191', api.nvim_get_current_line()) 1058 end) 1059 1060 it("handles bell respecting 'belloff' and 'visualbell'", function() 1061 local screen = Screen.new(50, 7) 1062 local chan = api.nvim_open_term(0, {}) 1063 1064 command('set belloff=') 1065 api.nvim_chan_send(chan, '\a') 1066 screen:expect(function() 1067 eq({ true, false }, { screen.bell, screen.visual_bell }) 1068 end) 1069 screen.bell = false 1070 1071 command('set visualbell') 1072 api.nvim_chan_send(chan, '\a') 1073 screen:expect(function() 1074 eq({ false, true }, { screen.bell, screen.visual_bell }) 1075 end) 1076 screen.visual_bell = false 1077 1078 command('set belloff=term') 1079 api.nvim_chan_send(chan, '\a') 1080 screen:expect({ 1081 condition = function() 1082 eq({ false, false }, { screen.bell, screen.visual_bell }) 1083 end, 1084 unchanged = true, 1085 }) 1086 1087 command('set belloff=all') 1088 api.nvim_chan_send(chan, '\a') 1089 screen:expect({ 1090 condition = function() 1091 eq({ false, false }, { screen.bell, screen.visual_bell }) 1092 end, 1093 unchanged = true, 1094 }) 1095 end) 1096 1097 it('does not wipeout unrelated buffer after channel closes', function() 1098 local screen = Screen.new(50, 7) 1099 screen:set_default_attr_ids({ 1100 [1] = { foreground = Screen.colors.Blue1, bold = true }, 1101 [2] = { reverse = true }, 1102 [31] = { background = Screen.colors.DarkGreen, foreground = Screen.colors.White, bold = true }, 1103 }) 1104 1105 local old_buf = api.nvim_get_current_buf() 1106 command('new') 1107 fn.chanclose(api.nvim_open_term(0, {})) 1108 local term_buf = api.nvim_get_current_buf() 1109 screen:expect([[ 1110 ^ | 1111 [Terminal closed] | 1112 {31:[Scratch] [-] }| 1113 | 1114 {1:~ }| 1115 {2:[No Name] }| 1116 | 1117 ]]) 1118 1119 -- Autocommand should not result in the wrong buffer being wiped out. 1120 command('autocmd TermLeave * ++once wincmd p') 1121 feed('ii') 1122 screen:expect([[ 1123 ^ | 1124 {1:~ }|*5 1125 | 1126 ]]) 1127 eq(old_buf, api.nvim_get_current_buf()) 1128 eq(false, api.nvim_buf_is_valid(term_buf)) 1129 1130 term_buf = api.nvim_get_current_buf() 1131 fn.chanclose(api.nvim_open_term(term_buf, {})) 1132 screen:expect([[ 1133 ^ | 1134 [Terminal closed] | 1135 |*5 1136 ]]) 1137 1138 -- Autocommand should not result in a heap UAF if it frees the terminal prematurely. 1139 command('autocmd TermLeave * ++once bwipeout!') 1140 feed('ii') 1141 screen:expect([[ 1142 ^ | 1143 {1:~ }|*5 1144 | 1145 ]]) 1146 eq(false, api.nvim_buf_is_valid(term_buf)) 1147 end) 1148 1149 local enew_screen = [[ 1150 ^ | 1151 {1:~ }|*5 1152 | 1153 ]] 1154 1155 local function test_enew_in_buf_with_running_term(env) 1156 describe('editing a new file', function() 1157 it('hides terminal buffer ignoring bufhidden=wipe', function() 1158 local old_snapshot = env.screen:get_snapshot() 1159 command('setlocal bufhidden=wipe') 1160 command('enew') 1161 neq(env.buf, api.nvim_get_current_buf()) 1162 env.screen:expect(enew_screen) 1163 feed('<C-^>') 1164 eq(env.buf, api.nvim_get_current_buf()) 1165 env.screen:expect(old_snapshot) 1166 end) 1167 end) 1168 end 1169 1170 local function test_open_term_in_buf_with_running_term(env) 1171 describe('does not allow opening another terminal', function() 1172 it('with jobstart() in same buffer', function() 1173 eq( 1174 ('Vim:Terminal already connected to buffer %d'):format(env.buf), 1175 pcall_err(fn.jobstart, { testprg('tty-test') }, { term = true }) 1176 ) 1177 env.screen:expect_unchanged() 1178 end) 1179 1180 it('with nvim_open_term() in same buffer', function() 1181 eq( 1182 ('Terminal already connected to buffer %d'):format(env.buf), 1183 pcall_err(api.nvim_open_term, env.buf, {}) 1184 ) 1185 env.screen:expect_unchanged() 1186 end) 1187 end) 1188 end 1189 1190 describe('with running terminal job', function() 1191 local env = {} 1192 1193 before_each(function() 1194 env.screen = Screen.new(50, 7) 1195 fn.jobstart({ testprg('tty-test') }, { term = true }) 1196 env.screen:expect([[ 1197 ^tty ready | 1198 |*6 1199 ]]) 1200 env.buf = api.nvim_get_current_buf() 1201 api.nvim_set_option_value('modified', false, { buf = env.buf }) 1202 end) 1203 1204 test_enew_in_buf_with_running_term(env) 1205 test_open_term_in_buf_with_running_term(env) 1206 end) 1207 1208 describe('with open nvim_open_term() channel', function() 1209 local env = {} 1210 1211 before_each(function() 1212 env.screen = Screen.new(50, 7) 1213 local chan = api.nvim_open_term(0, {}) 1214 api.nvim_chan_send(chan, 'TEST') 1215 env.screen:expect([[ 1216 ^TEST | 1217 |*6 1218 ]]) 1219 env.buf = api.nvim_get_current_buf() 1220 api.nvim_set_option_value('modified', false, { buf = env.buf }) 1221 end) 1222 1223 test_enew_in_buf_with_running_term(env) 1224 test_open_term_in_buf_with_running_term(env) 1225 end) 1226 1227 local function test_enew_in_buf_with_finished_term(env) 1228 describe('editing a new file', function() 1229 it('hides terminal buffer with bufhidden=hide', function() 1230 local old_snapshot = env.screen:get_snapshot() 1231 command('setlocal bufhidden=hide') 1232 command('enew') 1233 neq(env.buf, api.nvim_get_current_buf()) 1234 env.screen:expect(enew_screen) 1235 feed('<C-^>') 1236 eq(env.buf, api.nvim_get_current_buf()) 1237 env.screen:expect(old_snapshot) 1238 end) 1239 1240 it('wipes terminal buffer with bufhidden=wipe', function() 1241 command('setlocal bufhidden=wipe') 1242 command('enew') 1243 neq(env.buf, api.nvim_get_current_buf()) 1244 eq(false, api.nvim_buf_is_valid(env.buf)) 1245 env.screen:expect(enew_screen) 1246 feed('<C-^>') 1247 env.screen:expect([[ 1248 ^ | 1249 {1:~ }|*5 1250 {9:E23: No alternate file} | 1251 ]]) 1252 end) 1253 end) 1254 end 1255 1256 local function test_open_term_in_buf_with_finished_term(env) 1257 describe('does not leak memory when opening another terminal', function() 1258 describe('with jobstart() in same buffer', function() 1259 it('in Normal mode', function() 1260 fn.jobstart({ testprg('tty-test') }, { term = true }) 1261 env.screen:expect([[ 1262 ^tty ready | 1263 |*6 1264 ]]) 1265 end) 1266 1267 it('in Terminal mode', function() 1268 feed('i') 1269 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 1270 fn.jobstart({ testprg('tty-test') }, { term = true }) 1271 env.screen:expect([[ 1272 tty ready | 1273 ^ | 1274 |*4 1275 {5:-- TERMINAL --} | 1276 ]]) 1277 end) 1278 end) 1279 1280 describe('with nvim_open_term() in same buffer', function() 1281 it('in Normal mode', function() 1282 local chan = api.nvim_open_term(env.buf, {}) 1283 api.nvim_chan_send(chan, 'OTHER') 1284 env.screen:expect([[ 1285 ^OTHER | 1286 |*6 1287 ]]) 1288 end) 1289 1290 it('in Terminal mode', function() 1291 feed('i') 1292 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 1293 local chan = api.nvim_open_term(env.buf, {}) 1294 api.nvim_chan_send(chan, 'OTHER') 1295 env.screen:expect([[ 1296 OTHER^ | 1297 |*5 1298 {5:-- TERMINAL --} | 1299 ]]) 1300 end) 1301 end) 1302 end) 1303 end 1304 1305 describe('with exited terminal job', function() 1306 local env = {} 1307 1308 before_each(function() 1309 env.screen = Screen.new(50, 7) 1310 fn.jobstart({ testprg('shell-test') }, { term = true }) 1311 env.screen:expect([[ 1312 ^ready $ | 1313 [Process exited 0] | 1314 |*5 1315 ]]) 1316 env.buf = api.nvim_get_current_buf() 1317 api.nvim_set_option_value('modified', false, { buf = env.buf }) 1318 end) 1319 1320 test_enew_in_buf_with_finished_term(env) 1321 test_open_term_in_buf_with_finished_term(env) 1322 end) 1323 1324 describe('with closed nvim_open_term() channel', function() 1325 local env = {} 1326 1327 before_each(function() 1328 env.screen = Screen.new(50, 7) 1329 local chan = api.nvim_open_term(0, {}) 1330 api.nvim_chan_send(chan, 'TEST') 1331 fn.chanclose(chan) 1332 env.screen:expect([[ 1333 ^TEST | 1334 [Terminal closed] | 1335 |*5 1336 ]]) 1337 env.buf = api.nvim_get_current_buf() 1338 api.nvim_set_option_value('modified', false, { buf = env.buf }) 1339 end) 1340 1341 test_enew_in_buf_with_finished_term(env) 1342 test_open_term_in_buf_with_finished_term(env) 1343 end) 1344 1345 it('with nvim_open_term() channel and only 1 line is not reused by :enew', function() 1346 command('1new') 1347 local oldbuf = api.nvim_get_current_buf() 1348 api.nvim_open_term(oldbuf, {}) 1349 eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) 1350 feed('i') 1351 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 1352 feed([[<C-\><C-N>]]) 1353 eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) 1354 1355 command('enew') 1356 neq(oldbuf, api.nvim_get_current_buf()) 1357 eq({ mode = 'n', blocking = false }, api.nvim_get_mode()) 1358 feed('i') 1359 eq({ mode = 'i', blocking = false }, api.nvim_get_mode()) 1360 feed('<Esc>') 1361 eq({ mode = 'n', blocking = false }, api.nvim_get_mode()) 1362 1363 command('buffer #') 1364 eq(oldbuf, api.nvim_get_current_buf()) 1365 eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) 1366 feed('i') 1367 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 1368 feed([[<C-\><C-N>]]) 1369 eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) 1370 end) 1371 1372 it('does not allow OptionSet or b:term_title watcher to delete buffer', function() 1373 local au = api.nvim_create_autocmd('OptionSet', { command = 'bwipeout!' }) 1374 local chan = api.nvim_open_term(0, {}) 1375 matches('^E937: ', api.nvim_get_vvar('errmsg')) 1376 api.nvim_del_autocmd(au) 1377 api.nvim_set_vvar('errmsg', '') 1378 1379 api.nvim_chan_send(chan, '\027]2;SOME_TITLE\007') 1380 eq('SOME_TITLE', api.nvim_buf_get_var(0, 'term_title')) 1381 command([[call dictwatcheradd(b:, 'term_title', {-> execute('bwipe!')})]]) 1382 api.nvim_chan_send(chan, '\027]2;OTHER_TITLE\007') 1383 eq('OTHER_TITLE', api.nvim_buf_get_var(0, 'term_title')) 1384 matches('^E937: ', api.nvim_get_vvar('errmsg')) 1385 end) 1386 1387 it('using NameBuff in BufFilePre does not interfere with buffer rename', function() 1388 local oldbuf = api.nvim_get_current_buf() 1389 n.exec([[ 1390 file Xoldfile 1391 new Xotherfile 1392 wincmd w 1393 let g:BufFilePre_bufs = [] 1394 let g:BufFilePost_bufs = [] 1395 autocmd BufFilePre * call add(g:BufFilePre_bufs, [bufnr(), bufname()]) 1396 autocmd BufFilePost * call add(g:BufFilePost_bufs, [bufnr(), bufname()]) 1397 autocmd BufFilePre,BufFilePost * call execute('ls') 1398 ]]) 1399 fn.jobstart({ testprg('shell-test') }, { term = true }) 1400 eq({ { oldbuf, 'Xoldfile' } }, api.nvim_get_var('BufFilePre_bufs')) 1401 local buffilepost_bufs = api.nvim_get_var('BufFilePost_bufs') 1402 eq(1, #buffilepost_bufs) 1403 eq(oldbuf, buffilepost_bufs[1][1]) 1404 matches('^term://', buffilepost_bufs[1][2]) 1405 end) 1406 end) 1407 1408 describe('on_lines does not emit out-of-bounds line indexes when', function() 1409 before_each(function() 1410 clear() 1411 exec_lua([[ 1412 function _G.register_callback(bufnr) 1413 _G.cb_error = '' 1414 vim.api.nvim_buf_attach(bufnr, false, { 1415 on_lines = function(_, bufnr, _, firstline, _, _) 1416 local status, msg = pcall(vim.api.nvim_buf_get_offset, bufnr, firstline) 1417 if not status then 1418 _G.cb_error = msg 1419 end 1420 end 1421 }) 1422 end 1423 ]]) 1424 end) 1425 1426 it('creating a terminal buffer #16394', function() 1427 command('autocmd TermOpen * ++once call v:lua.register_callback(str2nr(expand("<abuf>")))') 1428 command('terminal') 1429 sleep(500) 1430 eq('', exec_lua([[return _G.cb_error]])) 1431 end) 1432 1433 it('deleting a terminal buffer #16394', function() 1434 command('terminal') 1435 sleep(500) 1436 local cb_error = exec_lua([[ 1437 _G.register_callback(0) 1438 vim.cmd('bdelete!') 1439 return _G.cb_error 1440 ]]) 1441 eq('', cb_error) 1442 end) 1443 end) 1444 1445 describe('terminal input', function() 1446 local chan --- @type integer 1447 1448 before_each(function() 1449 clear() 1450 chan = exec_lua(function() 1451 _G.input_data = '' 1452 return vim.api.nvim_open_term(0, { 1453 on_input = function(_, _, _, data) 1454 _G.input_data = _G.input_data .. data 1455 end, 1456 }) 1457 end) 1458 feed('i') 1459 poke_eventloop() 1460 end) 1461 1462 it('<C-Space> is sent as NUL byte', function() 1463 feed('aaa<C-Space>bbb') 1464 eq('aaa\0bbb', exec_lua([[return _G.input_data]])) 1465 end) 1466 1467 it('unknown special keys are not sent', function() 1468 feed('aaa<Help>bbb') 1469 eq('aaabbb', exec_lua([[return _G.input_data]])) 1470 end) 1471 1472 it('<Ignore> is no-op', function() 1473 feed('aaa<Ignore>bbb') 1474 eq('aaabbb', exec_lua([[return _G.input_data]])) 1475 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 1476 feed([[<C-\><Ignore><C-N>]]) 1477 eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) 1478 feed('v') 1479 eq({ mode = 'v', blocking = false }, api.nvim_get_mode()) 1480 feed('<Esc>') 1481 eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) 1482 feed('i') 1483 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 1484 feed([[<C-\><Ignore><C-O>]]) 1485 eq({ mode = 'ntT', blocking = false }, api.nvim_get_mode()) 1486 feed('v') 1487 eq({ mode = 'v', blocking = false }, api.nvim_get_mode()) 1488 feed('<Esc>') 1489 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 1490 fn.chanclose(chan) 1491 feed('<MouseMove>') 1492 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 1493 feed('<Ignore>') 1494 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 1495 eq('terminal', api.nvim_get_option_value('buftype', { buf = 0 })) 1496 feed('<Space>') 1497 eq({ mode = 'n', blocking = false }, api.nvim_get_mode()) 1498 eq('', api.nvim_get_option_value('buftype', { buf = 0 })) 1499 end) 1500 end) 1501 1502 describe('terminal input', function() 1503 it('sends various special keys with modifiers', function() 1504 clear() 1505 local screen = tt.setup_child_nvim({ 1506 '-u', 1507 'NONE', 1508 '-i', 1509 'NONE', 1510 '--cmd', 1511 'colorscheme vim', 1512 '--cmd', 1513 'set notermguicolors', 1514 '-c', 1515 'while 1 | redraw | echo keytrans(getcharstr(-1, #{simplify: 0})) | endwhile', 1516 }) 1517 screen:expect([[ 1518 ^ | 1519 {100:~ }|*3 1520 {3:[No Name] 0,0-1 All}| 1521 | 1522 {5:-- TERMINAL --} | 1523 ]]) 1524 local keys = { 1525 '<Tab>', 1526 '<CR>', 1527 '<Esc>', 1528 '<M-Tab>', 1529 '<M-CR>', 1530 '<M-Esc>', 1531 '<BS>', 1532 '<S-Tab>', 1533 '<Insert>', 1534 '<Del>', 1535 '<PageUp>', 1536 '<PageDown>', 1537 '<S-Up>', 1538 '<C-Up>', 1539 '<Up>', 1540 '<S-Down>', 1541 '<C-Down>', 1542 '<Down>', 1543 '<S-Left>', 1544 '<C-Left>', 1545 '<Left>', 1546 '<S-Right>', 1547 '<C-Right>', 1548 '<Right>', 1549 '<S-Home>', 1550 '<C-Home>', 1551 '<Home>', 1552 '<S-End>', 1553 '<C-End>', 1554 '<End>', 1555 '<C-LeftMouse><0,0>', 1556 '<C-LeftDrag><0,1>', 1557 '<C-LeftRelease><0,1>', 1558 '<2-LeftMouse><0,1>', 1559 '<2-LeftDrag><0,0>', 1560 '<2-LeftRelease><0,0>', 1561 '<M-MiddleMouse><0,0>', 1562 '<M-MiddleDrag><0,1>', 1563 '<M-MiddleRelease><0,1>', 1564 '<2-MiddleMouse><0,1>', 1565 '<2-MiddleDrag><0,0>', 1566 '<2-MiddleRelease><0,0>', 1567 '<S-RightMouse><0,0>', 1568 '<S-RightDrag><0,1>', 1569 '<S-RightRelease><0,1>', 1570 '<2-RightMouse><0,1>', 1571 '<2-RightDrag><0,0>', 1572 '<2-RightRelease><0,0>', 1573 '<S-X1Mouse><0,0>', 1574 '<S-X1Drag><0,1>', 1575 '<S-X1Release><0,1>', 1576 '<2-X1Mouse><0,1>', 1577 '<2-X1Drag><0,0>', 1578 '<2-X1Release><0,0>', 1579 '<S-X2Mouse><0,0>', 1580 '<S-X2Drag><0,1>', 1581 '<S-X2Release><0,1>', 1582 '<2-X2Mouse><0,1>', 1583 '<2-X2Drag><0,0>', 1584 '<2-X2Release><0,0>', 1585 '<S-ScrollWheelUp>', 1586 '<S-ScrollWheelDown>', 1587 '<ScrollWheelUp>', 1588 '<ScrollWheelDown>', 1589 '<S-ScrollWheelLeft>', 1590 '<S-ScrollWheelRight>', 1591 '<ScrollWheelLeft>', 1592 '<ScrollWheelRight>', 1593 } 1594 -- FIXME: The escape sequence to enable kitty keyboard mode doesn't work on Windows 1595 if not is_os('win') then 1596 table.insert(keys, '<C-I>') 1597 table.insert(keys, '<C-M>') 1598 table.insert(keys, '<C-[>') 1599 end 1600 for _, key in ipairs(keys) do 1601 feed(key) 1602 screen:expect(([[ 1603 | 1604 {100:~ }|*3 1605 {3:[No Name] 0,0-1 All}| 1606 %s^ {MATCH: *}| 1607 {5:-- TERMINAL --} | 1608 ]]):format(key:gsub('<%d+,%d+>$', ''))) 1609 end 1610 end) 1611 end) 1612 1613 if is_os('win') then 1614 describe(':terminal in Windows', function() 1615 local screen 1616 1617 before_each(function() 1618 clear() 1619 command('set modifiable swapfile undolevels=20') 1620 poke_eventloop() 1621 local cmd = { 'cmd.exe', '/K', 'PROMPT=$g$s' } 1622 screen = tt.setup_screen(nil, cmd) 1623 end) 1624 1625 it('"put" operator sends data normally', function() 1626 feed('<c-\\><c-n>G') 1627 local s = ':: tty ready' 1628 fn.setreg('a', s .. '\n:: appended ' .. s .. '\n\n') 1629 feed('"ap"ap') 1630 screen:expect([[ 1631 | 1632 > :: tty ready | 1633 > :: appended :: tty ready | 1634 > :: tty ready | 1635 > :: appended :: tty ready | 1636 ^> | 1637 | 1638 ]]) 1639 -- operator count is also taken into consideration 1640 feed('3"ap') 1641 screen:expect([[ 1642 > :: appended :: tty ready | 1643 > :: tty ready | 1644 > :: appended :: tty ready | 1645 > :: tty ready | 1646 > :: appended :: tty ready | 1647 ^> | 1648 | 1649 ]]) 1650 end) 1651 1652 it('":put" command sends data normally', function() 1653 feed('<c-\\><c-n>G') 1654 local s = ':: tty ready' 1655 fn.setreg('a', s .. '\n:: appended ' .. s .. '\n\n') 1656 feed(':put a<CR>') 1657 screen:expect([[ 1658 | 1659 > :: tty ready | 1660 > :: appended :: tty ready | 1661 > | 1662 | 1663 ^ | 1664 :put a | 1665 ]]) 1666 -- line argument is only used to move the cursor 1667 feed(':6put a<CR>') 1668 screen:expect([[ 1669 | 1670 > :: tty ready | 1671 > :: appended :: tty ready | 1672 > :: tty ready | 1673 > :: appended :: tty ready | 1674 ^> | 1675 :6put a | 1676 ]]) 1677 end) 1678 end) 1679 end 1680 1681 describe('termopen() (deprecated alias to `jobstart(…,{term=true})`)', function() 1682 before_each(clear) 1683 1684 it('disallowed when textlocked and in cmdwin buffer', function() 1685 command("autocmd TextYankPost <buffer> ++once call termopen('foo')") 1686 matches( 1687 'Vim%(call%):E565: Not allowed to change text or change window$', 1688 pcall_err(command, 'normal! yy') 1689 ) 1690 1691 feed('q:') 1692 eq( 1693 'Vim:E11: Invalid in command-line window; <CR> executes, CTRL-C quits', 1694 pcall_err(fn.termopen, 'bar') 1695 ) 1696 end) 1697 end) 1698 1699 describe('jobstart(…,{term=true})', function() 1700 before_each(clear) 1701 1702 describe('$COLORTERM value', function() 1703 before_each(function() 1704 -- Outer value should never be propagated to :terminal 1705 fn.setenv('COLORTERM', 'wrongvalue') 1706 end) 1707 1708 local function test_term_colorterm(expected, opts) 1709 local screen = Screen.new(50, 4) 1710 fn.jobstart({ 1711 nvim_prog, 1712 '-u', 1713 'NONE', 1714 '-i', 1715 'NONE', 1716 '--headless', 1717 '-c', 1718 'echo $COLORTERM | quit', 1719 }, vim.tbl_extend('error', opts, { term = true })) 1720 screen:expect(([[ 1721 ^%s{MATCH:%%s+}| 1722 [Process exited 0] | 1723 |*2 1724 ]]):format(expected)) 1725 end 1726 1727 describe("with 'notermguicolors'", function() 1728 before_each(function() 1729 command('set notermguicolors') 1730 end) 1731 it('is empty by default', function() 1732 test_term_colorterm('', {}) 1733 end) 1734 it('can be overridden', function() 1735 test_term_colorterm('expectedvalue', { env = { COLORTERM = 'expectedvalue' } }) 1736 end) 1737 end) 1738 1739 describe("with 'termguicolors'", function() 1740 before_each(function() 1741 command('set termguicolors') 1742 end) 1743 it('is "truecolor" by default', function() 1744 test_term_colorterm('truecolor', {}) 1745 end) 1746 it('can be overridden', function() 1747 test_term_colorterm('expectedvalue', { env = { COLORTERM = 'expectedvalue' } }) 1748 end) 1749 end) 1750 end) 1751 end)