inccommand_user_spec.lua (23315B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 local Screen = require('test.functional.ui.screen') 4 5 local api = n.api 6 local clear = n.clear 7 local eq = t.eq 8 local exec_lua = n.exec_lua 9 local insert = n.insert 10 local feed = n.feed 11 local command = n.command 12 local assert_alive = n.assert_alive 13 14 -- Implements a :Replace command that works like :substitute and has multibuffer support. 15 local setup_replace_cmd = [[ 16 local function show_replace_preview(use_preview_win, preview_ns, preview_buf, matches) 17 -- Find the width taken by the largest line number, used for padding the line numbers 18 local highest_lnum = math.max(matches[#matches][1], 1) 19 local highest_lnum_width = math.floor(math.log10(highest_lnum)) 20 local preview_buf_line = 0 21 local multibuffer = #matches > 1 22 23 for _, match in ipairs(matches) do 24 local buf = match[1] 25 local buf_matches = match[2] 26 27 if multibuffer and #buf_matches > 0 and use_preview_win then 28 local bufname = vim.api.nvim_buf_get_name(buf) 29 30 if bufname == "" then 31 bufname = string.format("Buffer #%d", buf) 32 end 33 34 vim.api.nvim_buf_set_lines( 35 preview_buf, 36 preview_buf_line, 37 preview_buf_line, 38 0, 39 { bufname .. ':' } 40 ) 41 42 preview_buf_line = preview_buf_line + 1 43 end 44 45 for _, buf_match in ipairs(buf_matches) do 46 local lnum = buf_match[1] 47 local line_matches = buf_match[2] 48 local prefix 49 50 if use_preview_win then 51 prefix = string.format( 52 '|%s%d| ', 53 string.rep(' ', highest_lnum_width - math.floor(math.log10(lnum))), 54 lnum 55 ) 56 57 vim.api.nvim_buf_set_lines( 58 preview_buf, 59 preview_buf_line, 60 preview_buf_line, 61 0, 62 { prefix .. vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] } 63 ) 64 end 65 66 for _, line_match in ipairs(line_matches) do 67 vim.api.nvim_buf_add_highlight( 68 buf, 69 preview_ns, 70 'Substitute', 71 lnum - 1, 72 line_match[1], 73 line_match[2] 74 ) 75 76 if use_preview_win then 77 vim.api.nvim_buf_add_highlight( 78 preview_buf, 79 preview_ns, 80 'Substitute', 81 preview_buf_line, 82 #prefix + line_match[1], 83 #prefix + line_match[2] 84 ) 85 end 86 end 87 88 preview_buf_line = preview_buf_line + 1 89 end 90 end 91 92 if use_preview_win then 93 return 2 94 else 95 return 1 96 end 97 end 98 99 local function do_replace(opts, preview, preview_ns, preview_buf) 100 local pat1 = opts.fargs[1] 101 102 if not pat1 then return end 103 104 local pat2 = opts.fargs[2] or '' 105 local line1 = opts.line1 106 local line2 = opts.line2 107 local matches = {} 108 109 -- Get list of valid and listed buffers 110 local buffers = vim.tbl_filter( 111 function(buf) 112 if not (vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buflisted and buf ~= preview_buf) 113 then 114 return false 115 end 116 117 -- Check if there's at least one window using the buffer 118 for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do 119 if vim.api.nvim_win_get_buf(win) == buf then 120 return true 121 end 122 end 123 124 return false 125 end, 126 vim.api.nvim_list_bufs() 127 ) 128 129 for _, buf in ipairs(buffers) do 130 local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, false) 131 local buf_matches = {} 132 133 for i, line in ipairs(lines) do 134 local startidx, endidx = 0, 0 135 local line_matches = {} 136 local num = 1 137 138 while startidx ~= -1 do 139 local match = vim.fn.matchstrpos(line, pat1, 0, num) 140 startidx, endidx = match[2], match[3] 141 142 if startidx ~= -1 then 143 line_matches[#line_matches+1] = { startidx, endidx } 144 end 145 146 num = num + 1 147 end 148 149 if #line_matches > 0 then 150 buf_matches[#buf_matches+1] = { line1 + i - 1, line_matches } 151 end 152 end 153 154 local new_lines = {} 155 156 for _, buf_match in ipairs(buf_matches) do 157 local lnum = buf_match[1] 158 local line_matches = buf_match[2] 159 local line = lines[lnum - line1 + 1] 160 local pat_width_differences = {} 161 162 -- If previewing, only replace the text in current buffer if pat2 isn't empty 163 -- Otherwise, always replace the text 164 if pat2 ~= '' or not preview then 165 if preview then 166 for _, line_match in ipairs(line_matches) do 167 local startidx, endidx = unpack(line_match) 168 local pat_match = line:sub(startidx + 1, endidx) 169 170 pat_width_differences[#pat_width_differences+1] = 171 #vim.fn.substitute(pat_match, pat1, pat2, 'g') - #pat_match 172 end 173 end 174 175 new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g') 176 end 177 178 -- Highlight the matches if previewing 179 if preview then 180 local idx_offset = 0 181 for i, line_match in ipairs(line_matches) do 182 local startidx, endidx = unpack(line_match) 183 -- Starting index of replacement text 184 local repl_startidx = startidx + idx_offset 185 -- Ending index of the replacement text (if pat2 isn't empty) 186 local repl_endidx 187 188 if pat2 ~= '' then 189 repl_endidx = endidx + idx_offset + pat_width_differences[i] 190 else 191 repl_endidx = endidx + idx_offset 192 end 193 194 if pat2 ~= '' then 195 idx_offset = idx_offset + pat_width_differences[i] 196 end 197 198 line_matches[i] = { repl_startidx, repl_endidx } 199 end 200 end 201 end 202 203 for lnum, line in pairs(new_lines) do 204 vim.api.nvim_buf_set_lines(buf, lnum - 1, lnum, false, { line }) 205 end 206 207 matches[#matches+1] = { buf, buf_matches } 208 end 209 210 if preview then 211 local lnum = vim.api.nvim_win_get_cursor(0)[1] 212 -- Use preview window only if preview buffer is provided and range isn't just the current line 213 local use_preview_win = (preview_buf ~= nil) and (line1 ~= lnum or line2 ~= lnum) 214 return show_replace_preview(use_preview_win, preview_ns, preview_buf, matches) 215 end 216 end 217 218 local function replace(opts) 219 do_replace(opts, false) 220 end 221 222 local function replace_preview(opts, preview_ns, preview_buf) 223 return do_replace(opts, true, preview_ns, preview_buf) 224 end 225 226 -- ":<range>Replace <pat1> <pat2>" 227 -- Replaces all occurrences of <pat1> in <range> with <pat2> 228 vim.api.nvim_create_user_command( 229 'Replace', 230 replace, 231 { nargs = '*', range = '%', addr = 'lines', 232 preview = replace_preview } 233 ) 234 ]] 235 236 describe("'inccommand' for user commands", function() 237 local screen 238 239 before_each(function() 240 clear() 241 screen = Screen.new(40, 17) 242 exec_lua(setup_replace_cmd) 243 command('set cmdwinheight=5') 244 insert [[ 245 text on line 1 246 more text on line 2 247 oh no, even more text 248 will the text ever stop 249 oh well 250 did the text stop 251 why won't it stop 252 make the text stop 253 ]] 254 end) 255 256 it("can preview 'nomodifiable' buffer", function() 257 exec_lua([[ 258 vim.api.nvim_create_user_command("PreviewTest", function() end, { 259 preview = function(ev) 260 vim.bo.modifiable = true 261 vim.api.nvim_buf_set_lines(0, 0, -1, false, {"cats"}) 262 return 2 263 end, 264 }) 265 ]]) 266 command('set inccommand=split') 267 268 command('set nomodifiable') 269 eq(false, api.nvim_get_option_value('modifiable', { buf = 0 })) 270 271 feed(':PreviewTest') 272 273 screen:expect([[ 274 cats | 275 {1:~ }|*8 276 {3:[No Name] [+] }| 277 | 278 {1:~ }|*4 279 {2:[Preview] }| 280 :PreviewTest^ | 281 ]]) 282 feed('<Esc>') 283 screen:expect([[ 284 text on line 1 | 285 more text on line 2 | 286 oh no, even more text | 287 will the text ever stop | 288 oh well | 289 did the text stop | 290 why won't it stop | 291 make the text stop | 292 ^ | 293 {1:~ }|*7 294 | 295 ]]) 296 297 eq(false, api.nvim_get_option_value('modifiable', { buf = 0 })) 298 end) 299 300 it('works with inccommand=nosplit', function() 301 command('set inccommand=nosplit') 302 feed(':Replace text cats') 303 screen:expect([[ 304 {10:cats} on line 1 | 305 more {10:cats} on line 2 | 306 oh no, even more {10:cats} | 307 will the {10:cats} ever stop | 308 oh well | 309 did the {10:cats} stop | 310 why won't it stop | 311 make the {10:cats} stop | 312 | 313 {1:~ }|*7 314 :Replace text cats^ | 315 ]]) 316 end) 317 318 it('works with inccommand=split', function() 319 command('set inccommand=split') 320 feed(':Replace text cats') 321 screen:expect([[ 322 {10:cats} on line 1 | 323 more {10:cats} on line 2 | 324 oh no, even more {10:cats} | 325 will the {10:cats} ever stop | 326 oh well | 327 did the {10:cats} stop | 328 why won't it stop | 329 make the {10:cats} stop | 330 | 331 {3:[No Name] [+] }| 332 |1| {10:cats} on line 1 | 333 |2| more {10:cats} on line 2 | 334 |3| oh no, even more {10:cats} | 335 |4| will the {10:cats} ever stop | 336 |6| did the {10:cats} stop | 337 {2:[Preview] }| 338 :Replace text cats^ | 339 ]]) 340 end) 341 342 it('properly closes preview when inccommand=split', function() 343 command('set inccommand=split') 344 feed(':Replace text cats<Esc>') 345 screen:expect([[ 346 text on line 1 | 347 more text on line 2 | 348 oh no, even more text | 349 will the text ever stop | 350 oh well | 351 did the text stop | 352 why won't it stop | 353 make the text stop | 354 ^ | 355 {1:~ }|*7 356 | 357 ]]) 358 end) 359 360 it('properly executes command when inccommand=split', function() 361 command('set inccommand=split') 362 feed(':Replace text cats<CR>') 363 screen:expect([[ 364 cats on line 1 | 365 more cats on line 2 | 366 oh no, even more cats | 367 will the cats ever stop | 368 oh well | 369 did the cats stop | 370 why won't it stop | 371 make the cats stop | 372 ^ | 373 {1:~ }|*7 374 :Replace text cats | 375 ]]) 376 end) 377 378 it('shows preview window only when range is not current line', function() 379 command('set inccommand=split') 380 feed('gg:.Replace text cats') 381 screen:expect([[ 382 {10:cats} on line 1 | 383 more text on line 2 | 384 oh no, even more text | 385 will the text ever stop | 386 oh well | 387 did the text stop | 388 why won't it stop | 389 make the text stop | 390 | 391 {1:~ }|*7 392 :.Replace text cats^ | 393 ]]) 394 end) 395 396 it('no crash on ambiguous command #18825', function() 397 command('set inccommand=split') 398 command('command Reply echo 1') 399 feed(':R') 400 assert_alive() 401 feed('e') 402 assert_alive() 403 end) 404 405 it('no crash if preview callback changes inccommand option', function() 406 command('set inccommand=nosplit') 407 exec_lua([[ 408 vim.api.nvim_create_user_command('Replace', function() end, { 409 nargs = '*', 410 preview = function() 411 vim.api.nvim_set_option_value('inccommand', 'split', {}) 412 return 2 413 end, 414 }) 415 ]]) 416 feed(':R') 417 assert_alive() 418 feed('e') 419 assert_alive() 420 end) 421 422 it('no crash when adding highlight after :substitute #21495', function() 423 command('set inccommand=nosplit') 424 exec_lua([[ 425 vim.api.nvim_create_user_command("Crash", function() end, { 426 preview = function(_, preview_ns, _) 427 vim.cmd("%s/text/cats/g") 428 vim.api.nvim_buf_add_highlight(0, preview_ns, "Search", 0, 0, -1) 429 return 1 430 end, 431 }) 432 ]]) 433 feed(':C') 434 screen:expect([[ 435 {10:cats on line 1} | 436 more cats on line 2 | 437 oh no, even more cats | 438 will the cats ever stop | 439 oh well | 440 did the cats stop | 441 why won't it stop | 442 make the cats stop | 443 | 444 {1:~ }|*7 445 :C^ | 446 ]]) 447 assert_alive() 448 end) 449 450 it('no crash if preview callback executes undo #20036', function() 451 command('set inccommand=nosplit') 452 exec_lua([[ 453 vim.api.nvim_create_user_command('Foo', function() end, { 454 nargs = '?', 455 preview = function(_, _, _) 456 vim.cmd.undo() 457 end, 458 }) 459 ]]) 460 461 -- Clear undo history 462 command('set undolevels=-1') 463 feed('ggyyp') 464 command('set undolevels=1000') 465 466 feed('yypp:Fo') 467 assert_alive() 468 feed('<Esc>:Fo') 469 assert_alive() 470 end) 471 472 local function test_preview_break_undo() 473 command('set inccommand=nosplit') 474 exec_lua([[ 475 vim.api.nvim_create_user_command('Test', function() end, { 476 nargs = 1, 477 preview = function(opts, _, _) 478 vim.cmd('norm i' .. opts.args) 479 return 1 480 end 481 }) 482 ]]) 483 feed(':Test a.a.a.a.') 484 screen:expect([[ 485 text on line 1 | 486 more text on line 2 | 487 oh no, even more text | 488 will the text ever stop | 489 oh well | 490 did the text stop | 491 why won't it stop | 492 make the text stop | 493 a.a.a.a. | 494 {1:~ }|*7 495 :Test a.a.a.a.^ | 496 ]]) 497 feed('<C-V><Esc>u') 498 screen:expect([[ 499 text on line 1 | 500 more text on line 2 | 501 oh no, even more text | 502 will the text ever stop | 503 oh well | 504 did the text stop | 505 why won't it stop | 506 make the text stop | 507 a.a.a. | 508 {1:~ }|*7 509 :Test a.a.a.a.{18:^[}u^ | 510 ]]) 511 feed('<Esc>') 512 screen:expect([[ 513 text on line 1 | 514 more text on line 2 | 515 oh no, even more text | 516 will the text ever stop | 517 oh well | 518 did the text stop | 519 why won't it stop | 520 make the text stop | 521 ^ | 522 {1:~ }|*7 523 | 524 ]]) 525 end 526 527 describe('breaking undo chain in Insert mode works properly', function() 528 it('when using i_CTRL-G_u #20248', function() 529 command('inoremap . .<C-G>u') 530 test_preview_break_undo() 531 end) 532 533 it('when setting &l:undolevels to itself #24575', function() 534 command('inoremap . .<Cmd>let &l:undolevels = &l:undolevels<CR>') 535 test_preview_break_undo() 536 end) 537 end) 538 539 it('disables preview if preview buffer cannot be created #27086', function() 540 command('set inccommand=split') 541 api.nvim_buf_set_name(0, '[Preview]') 542 exec_lua([[ 543 vim.api.nvim_create_user_command('Test', function() end, { 544 nargs = '*', 545 preview = function(_, _, _) 546 return 2 547 end 548 }) 549 ]]) 550 eq('split', api.nvim_get_option_value('inccommand', {})) 551 feed(':Test') 552 eq('nosplit', api.nvim_get_option_value('inccommand', {})) 553 end) 554 555 it('does not flush intermediate cursor position at end of message grid', function() 556 exec_lua([[ 557 vim.api.nvim_create_user_command('Test', function() end, { 558 nargs = '*', 559 preview = function(_, _, _) 560 vim.api.nvim_buf_set_text(0, 0, 0, 1, -1, { "Preview" }) 561 vim.cmd.sleep("1m") 562 return 1 563 end 564 }) 565 ]]) 566 local cursor_goto = screen._handle_grid_cursor_goto 567 screen._handle_grid_cursor_goto = function(...) 568 cursor_goto(...) 569 assert(screen._cursor.col < 12) 570 end 571 feed(':Test baz<Left><Left>arb') 572 screen:expect([[ 573 Preview | 574 oh no, even more text | 575 will the text ever stop | 576 oh well | 577 did the text stop | 578 why won't it stop | 579 make the text stop | 580 | 581 {1:~ }|*8 582 :Test barb^az | 583 ]]) 584 end) 585 586 it('works when CmdlineChanged calls wildtrigger() #35246', function() 587 api.nvim_buf_set_text(0, 0, 0, 1, -1, { '' }) 588 exec_lua([[ 589 vim.api.nvim_create_user_command("Repro", function() end, { 590 nargs = '+', 591 preview = function(opts, ns, buf) 592 vim.api.nvim_buf_set_lines(0, 0, -1, true, { opts.args }) 593 return 2 594 end 595 }) 596 ]]) 597 command([[autocmd CmdlineChanged [:/\?] call wildtrigger()]]) 598 command('set wildmode=noselect:lastused,full wildoptions=pum') 599 feed(':Repro ') 600 screen:expect([[ 601 | 602 {1:~ }|*15 603 :Repro ^ | 604 ]]) 605 feed('a') 606 screen:expect([[ 607 a | 608 {1:~ }|*15 609 :Repro a^ | 610 ]]) 611 feed('bc') 612 screen:expect([[ 613 abc | 614 {1:~ }|*15 615 :Repro abc^ | 616 ]]) 617 end) 618 619 it('no crash with % + preview + file completion #28851', function() 620 exec_lua([[ 621 local function callback() end 622 local function preview() 623 return 0 624 end 625 626 vim.api.nvim_create_user_command('TestCommand', callback, { 627 nargs = '?', 628 complete = 'file', 629 preview = preview, 630 }) 631 632 vim.cmd.edit('Xtestscript') 633 ]]) 634 feed(':TestCommand %') 635 assert_alive() 636 end) 637 end) 638 639 describe("'inccommand' with multiple buffers", function() 640 local screen 641 642 before_each(function() 643 clear() 644 screen = Screen.new(40, 17) 645 exec_lua(setup_replace_cmd) 646 command('set cmdwinheight=10') 647 insert [[ 648 foo bar baz 649 bar baz foo 650 baz foo bar 651 ]] 652 command('vsplit | enew') 653 insert [[ 654 bar baz foo 655 baz foo bar 656 foo bar baz 657 ]] 658 end) 659 660 it('works', function() 661 command('set inccommand=nosplit') 662 feed(':Replace foo bar') 663 screen:expect([[ 664 bar baz {10:bar} │{10:bar} bar baz | 665 baz {10:bar} bar │bar baz {10:bar} | 666 {10:bar} bar baz │baz {10:bar} bar | 667 │ | 668 {1:~ }│{1:~ }|*11 669 {3:[No Name] [+] }{2:[No Name] [+] }| 670 :Replace foo bar^ | 671 ]]) 672 feed('<CR>') 673 screen:expect([[ 674 bar baz bar │bar bar baz | 675 baz bar bar │bar baz bar | 676 bar bar baz │baz bar bar | 677 ^ │ | 678 {1:~ }│{1:~ }|*11 679 {3:[No Name] [+] }{2:[No Name] [+] }| 680 :Replace foo bar | 681 ]]) 682 end) 683 684 it('works with inccommand=split', function() 685 command('set inccommand=split') 686 feed(':Replace foo bar') 687 screen:expect([[ 688 bar baz {10:bar} │{10:bar} bar baz | 689 baz {10:bar} bar │bar baz {10:bar} | 690 {10:bar} bar baz │baz {10:bar} bar | 691 │ | 692 {3:[No Name] [+] }{2:[No Name] [+] }| 693 Buffer #1: | 694 |1| {10:bar} bar baz | 695 |2| bar baz {10:bar} | 696 |3| baz {10:bar} bar | 697 Buffer #2: | 698 |1| bar baz {10:bar} | 699 |2| baz {10:bar} bar | 700 |3| {10:bar} bar baz | 701 | 702 {1:~ }| 703 {2:[Preview] }| 704 :Replace foo bar^ | 705 ]]) 706 feed('<CR>') 707 screen:expect([[ 708 bar baz bar │bar bar baz | 709 baz bar bar │bar baz bar | 710 bar bar baz │baz bar bar | 711 ^ │ | 712 {1:~ }│{1:~ }|*11 713 {3:[No Name] [+] }{2:[No Name] [+] }| 714 :Replace foo bar | 715 ]]) 716 end) 717 end)